Bỏ qua đến nội dung chính
Học Python
Logo Python phía trên cửa sổ code nối với biểu đồ cột

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, self là 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 TypeError hoặc AttributeError: quên self, tạo object thiếu tham số và quên gọi super().__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:

python
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.

Terminal chạy b1_class_dau_tien.py bằng Python 3.15.1: in Áo thun form rộng 259000, Quần jean cao cấp 749000, 777000, rồi dòng tên class SanPham kèm địa chỉ bộ nhớ 0x75bbcdd58980 và dòng kiểu class SanPham

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ị:

python
    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:

Terminal chạy b2_str.py: print một sản phẩm ra SP0005 Áo khoác form rộng: 799,000đ, print cả list ra dạng SanPham('SP0005', 'Áo khoác form rộng', 799000), sắp theo giá ra SP0001 259,000đ, SP0003 649,000đ, SP0005 799,000đ

Đị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:

python
    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:

Terminal chạy loi_quen_self.py: Traceback ở dòng 12 print(ao.mo_ta()), TypeError: SanPham.mo_ta() takes 0 positional arguments but 1 was given

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:

python
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:

python
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)
Terminal: loi_gio_hang_chung.py in Giỏ của Bình có Áo thun form rộng số lượng 2 và Cùng một list? True; b3_gio_hang.py in An 2 món, 1,167,000đ và Bình 1 món, 1,947,000đ

Ở 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:

python
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:

python
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__)
Terminal chạy b4_ke_thua.py: Áo thun form rộng 518,000đ, Áo khoác form rộng 1,278,400đ, isinstance in True False, thứ tự MRO in SanPhamGiamGia, SanPham, object

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:

Terminal chạy loi_quen_super.py: in 20 cho phan_tram, sau đó Traceback ở dòng 15 print(ao_sale.gia), AttributeError: 'SanPhamGiamGia' object has no attribute 'gia'

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:

python
    @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:

Terminal chạy b5_property.py: in 239000, rồi Traceback ở dòng 21 ao.gia = -5000, đi vào setter dòng 14, ValueError: Giá không được âm: -5000

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:

python
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:

Terminal: b6_dataclass.py in SanPham(ma='SP0001', ten='Áo thun form rộng', gia=259000), True, 777000; b6b_so_sanh_class.py với class thường in False và dict thuộc tính gồm ma, ten, gia

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ị:

Biểu đồ RAM cho 1 triệu sản phẩm trên Python 3.15.1 ngày 26/10/2026: dict 192,4 MB, class 104,4 MB, dataclass 104,4 MB, namedtuple 88,4 MB, class có slots 64,4 MB, dataclass slots 64,4 MB

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

@dataclass

104,4 MB

0,54 giây

namedtuple

88,4 MB

0,65 giây

class có __slots__

64,4 MB

0,46 giây

@dataclass(slots=True)

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 self trong phương thức

TypeError: ... takes 0 positional arguments but 1 was given

thêm self làm tham số đầu

Tạo object thiếu tham số

TypeError: ... missing 1 required positional argument

truyền đủ tham số mà __init__ cần

Class con quên super().__init__

AttributeError: ... object has no attribute 'gia'

gọi super().__init__(...) đầu __init__ của class con

Đặt list ở cấp class

không báo lỗi, các object dùng chung list

tạo list trong __init__ qua self

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á:

text
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:

text
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

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.

Tham vấn chuyên môn

Nguyễn Việt Chung

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.
Biên soạn: AIDev Team · Rà soát · Quy trình biên tập