
Class và OOP trong Python: viết lớp đầu tiên
AIDev TeamTham vấn chuyên môn: Nguyễn Việt Chung
Đăng 12 phút đọc1 lượt xem
Mục lục · 21
Class trong Python là khuôn để tạo object gom dữ liệu và hàm xử lý dữ liệu đó vào một chỗ. Mình viết class SanPham và GioHang cho một cửa hàng mẫu và chạy thật trên Python 3.15.1. Bài tái hiện 4 lỗi người mới hay vấp, rồi đo 1 triệu object theo 6 cách lưu để xem class, dict và __slots__ tốn bao nhiêu RAM.
Class trong Python là cái khuôn: bạn mô tả một loại đối tượng có những dữ liệu gì (thuộc tính) và làm được gì (phương thức), rồi từ khuôn đó tạo ra bao nhiêu object (đối tượng cụ thể) tùy ý. OOP (lập trình hướng đối tượng) là cách tổ chức chương trình xoay quanh các object như vậy. Bài này đi từ class Python đầu tiên đến kế thừa, @property và dataclass, mọi ví dụ đều chạy thật. Phần lý thuyết đầy đủ nằm ở chương Classes trong tài liệu Python.
Mình dùng ví dụ cửa hàng Tiệm Mây (cửa hàng mẫu trong các khóa học của AIDev), chạy trên Python 3.15.1 ngày 26/10/2026 trên một máy AIDev Cloud chạy Ubuntu 24.04. Vài điều mình rút ra:
__init__chạy mỗi lần bạn tạo object,selflà chính object đang được tạo hoặc đang gọi phương thức.Trong bốn lỗi mình tái hiện, ba lỗi báo
TypeErrorhoặcAttributeError: quênself, tạo object thiếu tham số và quên gọisuper().__init__. Lỗi thứ tư không báo gì cả.Lưu 1 triệu sản phẩm bằng dict tốn 192,4 MB RAM, bằng class tốn 104,4 MB, thêm
__slots__còn 64,4 MB.
Class là gì, khác hàm ở chỗ nào?
Hàm gom các bước xử lý, còn class gom cả dữ liệu lẫn các hàm làm việc với dữ liệu đó. Khi chỉ có dict và hàm, dữ liệu sản phẩm nằm một nơi, hàm tính tiền nằm một nơi khác, và không có gì bảo đảm dict nào cũng đủ khóa gia. Class đặt hai thứ vào cùng một chỗ.
Bạn đã dùng object ngay từ những dòng Python đầu tiên mà có thể không để ý: "abc".upper() là gọi phương thức upper của một object kiểu str. Viết class là tự tạo kiểu dữ liệu của riêng bạn.
Viết class Python đầu tiên thế nào?
Class SanPham dưới đây có ba thuộc tính (mã, tên, giá) và một phương thức tính thành tiền:
class SanPham:
def __init__(self, ma, ten, gia):
self.ma = ma
self.ten = ten
self.gia = gia
def thanh_tien(self, so_luong):
return self.gia * so_luong
ao = SanPham("SP0001", "Áo thun form rộng", 259000)
quan = SanPham("SP0008", "Quần jean cao cấp", 749000)
print(ao.ten, ao.gia)
print(quan.ten, quan.gia)
print(ao.thanh_tien(3))
print(ao)
print(type(ao))
Dòng SanPham("SP0001", ...) tạo một object mới. Python tự gọi __init__, truyền object vừa tạo vào self, và ba dòng self.ma = ma gắn dữ liệu lên object đó. Khi gọi ao.thanh_tien(3), Python hiểu thành SanPham.thanh_tien(ao, 3), nên self bên trong chính là ao.

Hai object ao và quan dùng chung một khuôn nhưng giữ dữ liệu riêng. Dòng in print(ao) thì chưa giúp được gì: Python chỉ in tên class và địa chỉ bộ nhớ.
In object ra cho dễ đọc bằng __str__ và __repr__
Thêm hai phương thức đặc biệt này vào class SanPham thì print in ra thông tin sản phẩm thay cho địa chỉ bộ nhớ. __str__ dùng khi in cho người đọc, __repr__ dùng khi object nằm trong list hoặc khi bạn gõ tên biến trong cửa sổ tương tác. Dấu !r trong f-string in giá trị kèm dấu nháy, đúng dạng Python hiển thị:
def __str__(self):
return f"{self.ma} {self.ten}: {self.gia:,}đ"
def __repr__(self):
return f"SanPham({self.ma!r}, {self.ten!r}, {self.gia})"
Mình tạo một list ba sản phẩm rồi sắp theo giá bằng sorted(ds, key=lambda sp: sp.gia), tức là lấy thuộc tính gia của từng sản phẩm làm khóa để sắp:

Định dạng {:,} của Python dùng dấu phẩy ngăn hàng nghìn kiểu Mỹ. Muốn in 259.000đ kiểu Việt Nam, bạn đổi dấu sau khi định dạng, ví dụ f"{gia:,}".replace(",", ".").
self trong Python là gì, quên self thì sao?
self là tham số đầu tiên của mọi phương thức thường, nhận object đang gọi phương thức. Tên self chỉ là quy ước, nhưng cả cộng đồng Python đều dùng tên này. Mình cố tình viết một phương thức quên self:
def mo_ta():
return "Sản phẩm của Tiệm Mây"
Gọi ao.mo_ta() thì Python vẫn tự truyền ao vào làm tham số đầu tiên, trong khi hàm không nhận tham số nào:

Dòng lỗi nói hàm nhận 0 tham số nhưng được đưa 1. Tham số được đưa vào ở đây chính là self. Thêm self vào def mo_ta(self): là hết lỗi.
Nhiều object dùng chung một class có ảnh hưởng nhau không?
Không, nếu dữ liệu được gán qua self trong __init__. Có, nếu bạn khai báo dữ liệu đó ngay dưới dòng class. Đây là lỗi mình thấy khó nhận ra nhất vì code chạy không báo gì. Class GioHang dưới đây đặt list cac_mon ở cấp class:
class GioHang:
cac_mon = [] # đặt ở đây là dùng chung cho MỌI giỏ
def them(self, ten, so_luong):
self.cac_mon.append((ten, so_luong))
Mình tạo giỏ của An và giỏ của Bình, chỉ thêm hàng vào giỏ của An. Giỏ của Bình cũng có món đó, vì hai giỏ cùng trỏ vào một list. Bản sửa đưa self.cac_mon = [] vào __init__, để mỗi lần tạo giỏ là một list mới:
class GioHang:
def __init__(self, khach):
self.khach = khach
self.cac_mon = [] # mỗi giỏ một list riêng
def them(self, san_pham, so_luong=1):
self.cac_mon.append((san_pham, so_luong))
def tong_tien(self):
return sum(sp.thanh_tien(sl) for sp, sl in self.cac_mon)

Ở bản sửa, giỏ của An có 2 áo thun và 1 váy liền, tổng 1.167.000đ. Giỏ của Bình có 3 váy liền, tổng 1.947.000đ. Số món in ra là số dòng hàng trong giỏ, nên 3 váy liền vẫn tính là 1 món. GioHang.tong_tien gọi thanh_tien của từng SanPham, tức là các object làm việc với nhau qua phương thức của nhau.
Kế thừa trong Python viết thế nào?
Kế thừa (inheritance) cho một class mới dùng lại mọi thứ của class có sẵn rồi thêm hoặc sửa phần khác. SanPhamGiamGia dưới đây kế thừa SanPham, thêm phần trăm giảm và viết lại cách tính thành tiền:
class SanPhamGiamGia(SanPham):
def __init__(self, ma, ten, gia, phan_tram):
super().__init__(ma, ten, gia)
self.phan_tram = phan_tram
def thanh_tien(self, so_luong):
goc = super().thanh_tien(so_luong)
return round(goc * (100 - self.phan_tram) / 100)
super() gọi phương thức cùng tên của class cha. Một vòng lặp gọi thanh_tien(2) trên cả hai loại sản phẩm mà không cần biết object nào là loại nào:
ao = SanPham("SP0001", "Áo thun form rộng", 259000)
ao_sale = SanPhamGiamGia("SP0005", "Áo khoác form rộng", 799000, 20)
for sp in [ao, ao_sale]:
print(sp.ten, f"{sp.thanh_tien(2):,}đ")
print(isinstance(ao_sale, SanPham), isinstance(ao, SanPhamGiamGia))
print(SanPhamGiamGia.__mro__)

Hai áo khoác giá 799.000đ giảm 20% còn 1.278.400đ. isinstance(ao_sale, SanPham) ra True vì sản phẩm giảm giá vẫn là một sản phẩm, còn chiều ngược lại ra False. Dòng __mro__ là thứ tự Python tìm phương thức: class con trước, rồi class cha, cuối cùng là object.
Quên gọi super().__init__
Nếu __init__ của class con không gọi super().__init__(...), các dòng self.ma, self.ten, self.gia của class cha không bao giờ chạy. Object vẫn được tạo, chỉ đến lúc đọc gia mới lỗi:

Chặn dữ liệu sai bằng @property
Với class trên, không gì cấm bạn gán ao.gia = -5000. @property biến gia thành một thuộc tính có kiểm tra: đọc thì gọi hàm getter, gán thì gọi hàm setter. Mình để setter báo lỗi khi giá âm:
@property
def gia(self):
return self._gia
@gia.setter
def gia(self, gia_moi):
if gia_moi < 0:
raise ValueError(f"Giá không được âm: {gia_moi}")
self._gia = gia_moi
Dòng self.gia = gia trong __init__ cũng đi qua setter, nên tạo sản phẩm giá âm cũng bị chặn. Gán 239.000 thì được, gán -5.000 thì dừng ngay ở dòng gán:

dataclass có thay được class thường không?
Với class chủ yếu để chứa dữ liệu như SanPham, @dataclass trong module dataclasses tự viết __init__, __repr__ và phép so sánh == từ danh sách trường bạn khai báo:
from dataclasses import dataclass
@dataclass
class SanPham:
ma: str
ten: str
gia: int
def thanh_tien(self, so_luong):
return self.gia * so_luong
Mình tạo hai sản phẩm giống hệt nhau rồi so bằng ==. Với dataclass kết quả là True. Với class thường viết tay ở đầu bài, kết quả là False, vì Python mặc định so xem hai biến có trỏ vào cùng một object không:

Mình dùng dataclass cho class chứa dữ liệu, và class thường khi __init__ cần làm nhiều việc hơn gán trường, ví dụ mở file hay kiểm tra nhiều điều kiện cùng lúc. Chú thích kiểu str, int trong dataclass chỉ là ghi chú cho người đọc code: Python không chặn bạn truyền chuỗi vào trường gia.
Object của class tốn bao nhiêu RAM so với dict?
Mình tạo 1 triệu bản ghi sản phẩm (mã, tên, giá) theo 6 cách, trong đó namedtuple là tuple có tên cho từng trường (module collections), và đo RAM bằng tracemalloc, module đo bộ nhớ có sẵn của Python. Mỗi cách chạy trong một process (tiến trình) riêng, 3 lần, lấy trung vị:

Cách lưu | RAM cho 1 triệu bản ghi | Thời gian tạo |
|---|---|---|
dict | 192,4 MB | 0,55 giây |
class thường | 104,4 MB | 0,54 giây |
| 104,4 MB | 0,54 giây |
| 88,4 MB | 0,65 giây |
class có | 64,4 MB | 0,46 giây |
| 64,4 MB | 0,47 giây |
Trên Python 3.15.1, một object của class thường tốn khoảng 104 byte (con số này đã gồm phần list chứa 1 triệu object, chưa gồm chuỗi mã và tên sản phẩm vì các chuỗi này tạo sẵn trước khi đo), dict tốn gần gấp đôi. __slots__ = ("ma", "ten", "gia") khai báo trước danh sách thuộc tính, nên object bỏ được phần lưu thuộc tính kiểu dict, 1 triệu object nhẹ thêm 40 MB. Đổi lại, bạn không gắn thêm được thuộc tính nào ngoài danh sách đó. Với vài trăm sản phẩm thì chênh lệch này không đáng kể. Bạn chỉ cần nghĩ tới khi giữ hàng triệu object trong RAM.
Lỗi thường gặp khi viết class Python
Bốn lỗi dưới đây là những lỗi mình cố tình tái hiện trong bài, kèm thông báo Python in ra:
Lỗi | Thông báo | Cách sửa |
|---|---|---|
Quên |
| thêm |
Tạo object thiếu tham số |
| truyền đủ tham số mà |
Class con quên |
| gọi |
Đặt list ở cấp class | không báo lỗi, các object dùng chung list | tạo list trong |
Lỗi thiếu tham số xảy ra khi mình gọi SanPham("SP0001", "Áo thun form rộng") mà quên giá:
Traceback (most recent call last):
File "/root/oop/loi_thieu_tham_so.py", line 8, in <module>
ao = SanPham("SP0001", "Áo thun form rộng")
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: SanPham.__init__() missing 1 required positional argument: 'gia'
Từ Python 3.11, traceback có thêm dòng dấu ~ và ^ chỉ đúng chỗ gọi bị lỗi. Tên tham số còn thiếu ('gia') nằm ở cuối dòng TypeError, nên bạn đọc dòng cuối trước.
Lỗi quên super().__init__ chỉ hiện ra khi code đọc tới thuộc tính của class cha, có khi cách xa dòng tạo object:
Traceback (most recent call last):
File "/root/oop/loi_quen_super.py", line 15, in <module>
print(ao_sale.gia)
^^^^^^^^^^^
AttributeError: 'SanPhamGiamGia' object has no attribute 'gia'
Thấy AttributeError trên object của class con mà thuộc tính đó được gán ở class cha, bạn kiểm tra ngay __init__ của class con.
Khi nào không nên viết class?
Script vài chục dòng đọc một file rồi in kết quả thì hàm là đủ, thêm class chỉ làm code dài ra. Dữ liệu dạng bảng hàng nghìn dòng trở lên thì dùng pandas DataFrame, nhanh và gọn hơn một list object. Class đáng viết khi một nhóm dữ liệu luôn đi cùng một nhóm hàm xử lý, như sản phẩm và giỏ hàng trong bài, hoặc khi bạn cần nhiều biến thể của cùng một thứ như sản phẩm thường và sản phẩm giảm giá.
Câu hỏi thường gặp
__init__ trong Python dùng để làm gì?
__init__ chạy tự động ngay sau khi object được tạo, để gán dữ liệu ban đầu cho object. Tham số của __init__ (trừ self) chính là những gì bạn phải truyền khi tạo object, thiếu một cái là Python báo TypeError như mục lỗi ở trên.
Đặt tên class thế nào cho đúng?
Theo PEP 8 (quy ước viết code Python), tên class viết kiểu CapWords (hay gọi là PascalCase): mỗi từ viết hoa chữ đầu, không có dấu gạch dưới, như SanPham, GioHang. Tên phương thức và biến viết thường, nối bằng gạch dưới như thanh_tien.
Biến của class và biến của object khác nhau thế nào?
Biến khai báo ngay dưới dòng class thuộc về class, mọi object dùng chung, như list cac_mon trong lỗi giỏ hàng ở trên. Biến gán qua self. trong phương thức thuộc về từng object. Biến cấp class hợp với thứ thật sự dùng chung, như một bộ đếm số sản phẩm đã tạo.
OOP có bắt buộc khi học Python không?
Bạn viết được nhiều chương trình hữu ích mà không cần tự viết class. Nhưng gần như mọi library lớn đều dùng OOP: requests.Session, pandas.DataFrame hay torch.nn.Module của PyTorch đều là class. Hiểu class giúp bạn đọc tài liệu và lỗi của các library đó dễ hơn nhiều.
Có nên dùng dataclass thay cho mọi class?
Không. Dataclass hợp với class chủ yếu chứa dữ liệu. Class có __init__ phức tạp hoặc cần kiểm soát kỹ việc gán thuộc tính thì viết class thường, kết hợp @property như mục chặn giá âm.
Đọc tiếp
Học Python từ số 0: lộ trình 8 chặng, OOP là chặng cuối sau hàm, file và module.
Chương trình Python mẫu cho người mới: 15 chương trình chạy thật để luyện trước khi viết class.
Môi trường ảo Python (venv): tạo môi trường riêng cho project trước khi cài library.
Bài tập Python có lời giải: bài luyện kèm bộ chấm tự động.
Ebook Python từ số 0 của AIDev có một chương riêng về OOP và Git, sau đó dùng chính các class đó trong project quản lý chi tiêu ở chương cuối. Nếu bạn học Python để đi tiếp sang dữ liệu và AI, lộ trình AI Engineer từ số 0 bắt đầu từ khóa Python cho người mới, trong đó có phần class và object với bài tập chấm tự động.
Mình chạy ngày 26/10/2026 trên máy AIDev Cloud: Ubuntu 24.04.4 LTS, Intel Xeon E5-2696 v4, Python 3.15.1 (cài bằng uv 0.13.5) trong môi trường ảo venv.
Chuyên mục
Học PythonTham vấn chuyên môn
Nhà sáng lập AIDev và VietPlatform · Thạc sĩ AI MSE
- Tốt nghiệp thủ khoa xuất sắc Thạc sĩ AI MSE.
- Đồng sáng lập, CTO E-University, SVOnline: số hóa 10+ đại học, 40.000+ sinh viên.
- Xây hệ thống AI cho doanh nghiệp tại VietPlatform.


