← Lập trình Python nâng cao

Bài 1 · Nâng cao · 24 phút· Cập nhật 11/06/2026

OOP & mô hình dữ liệu

Biên soạn bởi Nguyễn Anh Tuấn

Học OOP Python: class & object, phương thức dunder (data model), @property, dataclass - dạy object của bạn cư xử như kiểu dựng sẵn.

Một class là bản thiết kế; mỗi object gói dữ liệu (thuộc tính) và hành vi (phương thức). __init__ chạy khi tạo object; self là chính object đó:

dem.py

class BoDem:
    def __init__(self, batdau=0):   # chay khi tao object
        self.so = batdau            # thuoc tinh cua THUC THE

    def tang(self):                 # self = chinh object
        self.so += 1

d = BoDem()
d.tang(); d.tang()                  # BoDem.tang(d)
print(d.so)

Kết quả khi chạy

2
  • __init__ khởi tạo trạng thái; self là object đang được thao tác.
  • d.tang() thực chất là BoDem.tang(d) - Python tự truyền d vào self.
  • Thuộc tính của thực thể (self.so) là RIÊNG mỗi object.

Cú pháp quen thuộc - print(p), p1 == p2, p1 + p2 - thực ra gọi các dunder của object. Bật/tắt định nghĩa dunder để thấy khác biệt:

Lớp Diem:
class Diem:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):                 # repr(p), và print khi thiếu __str__
        return f"Diem({self.x}, {self.y})"

    def __eq__(self, other):            # p1 == p2
        return (self.x, self.y) == (other.x, other.y)

    def __add__(self, other):           # p1 + p2
        return Diem(self.x + other.x, self.y + other.y)
print(p1) gọi __str__ → __repr__
→ Diem(1, 2)

print gọi __str__ (thiếu thì lùi về __repr__). Không định nghĩa → bản mặc định khó đọc.

p1 == p2 gọi __eq__
→ True

p1, p2 cùng toạ độ. Có __eq__ → so theo GIÁ TRỊ (True). Không có → so theo DANH TÍNH: hai object khác nhau → False.

p1 + p3 gọi __add__
→ Diem(4, 6)

Toán tử + gọi __add__. Không định nghĩa → Python không biết cộng hai Diem → TypeError.

repr(p1) gọi __repr__
→ 'Diem(1, 2)'

repr() cho str “kỹ thuật” để lập trình viên đọc - lý tưởng là tái tạo được object.

★ Toán tử & hàm dựng sẵn chỉ là “lớp áo”: chúng GỌI các phương thức dunder của object. Định nghĩa dunder = dạy object cư xử như kiểu có sẵn.
  • len(x) → x.__len__(); a + b → a.__add__(b); print(x) → x.__str__() (thiếu thì __repr__).
  • Định nghĩa dunder = dạy object của bạn cư xử như kiểu dựng sẵn.
  • Không định nghĩa → hành vi mặc định (in khó đọc, == theo danh tính, + báo lỗi).

__repr__ vs __str__

__str__ = bản cho người dùng (print). __repr__ = bản kỹ thuật cho lập trình viên (repr/REPL/debug). Chỉ viết một cái? Chọn __repr__ - vì thiếu __str__ thì Python lùi về nó.

Mô hình dữ liệu mạnh nhất khi object của bạn hành xử như kiểu dựng sẵn. Viết vài dunder là len(), [], in, vòng for đều chạy:

container.py

class Tui:
    def __init__(self, *items):
        self._xs = list(items)
    def __len__(self):              # len(t)
        return len(self._xs)
    def __getitem__(self, i):       # t[i]  (va for tu suy ra neu thieu __iter__)
        return self._xs[i]
    def __contains__(self, x):      # x in t
        return x in self._xs

t = Tui("a", "b", "c")
print(len(t), t[0], "b" in t, [x.upper() for x in t])

Kết quả khi chạy

3 a True ['A', 'B', 'C']
  • __len__ → len(); __getitem__ → t[i]; __contains__ → “x in t”.
  • Thiếu __iter__, vòng for vẫn chạy bằng cách gọi __getitem__ với 0,1,2… - nhưng nên thêm __iter__ cho rõ & nhanh.
  • Viết đủ giao thức → object “của bạn” dùng y như list/dict dựng sẵn.

@property cho truy cập như thuộc tính nhưng chạy hàm bên dưới - để tính toán hoặc kiểm soát khi gán (validate):

property.py

class TaiKhoan:
    def __init__(self, sodu):
        self._sodu = sodu

    @property
    def sodu(self):             # doc: tk.sodu
        return self._sodu

    @sodu.setter
    def sodu(self, v):          # ghi: tk.sodu = ...
        if v < 0:
            raise ValueError("so du am!")   # validate khi gan
        self._sodu = v

tk = TaiKhoan(100)
tk.sodu = 50; print(tk.sodu)
tk.sodu = -1                    # -> ValueError

Kết quả khi chạy

50
ValueError: so du am!
  • @property = getter (đọc như thuộc tính); @ten.setter = ghi có kiểm soát.
  • Chỉ getter → thuộc tính “chỉ đọc”; thêm setter để validate/chuẩn hoá khi gán.
  • Lợi: thêm logic mà KHÔNG đổi mã nơi dùng (tk.sodu vẫn như trước).

Ngoài việc bớt boilerplate, @dataclass còn nhiều tuỳ chọn mạnh:

dataclass.py

from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)     # bat bien + so sanh duoc
class Diem:
    x: int
    y: int
    tags: list = field(default_factory=list)   # moi object mot list MOI
    def __post_init__(self):
        if not self.tags:
            object.__setattr__(self, "tags", ["mac-dinh"])  # frozen -> dung cach nay

a, b = Diem(1, 2), Diem(1, 3)
print(a < b, a.tags)            # order=True -> so sanh theo (x, y, tags)
a.x = 9                         # frozen -> loi

Kết quả khi chạy

True ['mac-dinh']
FrozenInstanceError
  • frozen=True: object bất biến (gán lại → FrozenInstanceError) và hashable.
  • order=True: tự sinh __lt__… → so sánh & sorted được.
  • field(default_factory=list): MỖI object một list mới - tránh bẫy mặc định mutable.
  • __post_init__: chạy sau __init__ để validate/tính phái sinh.

Thuộc tính của một object thường sống trong một dict ẩn (__dict__) - linh hoạt nhưng tốn bộ nhớ. Với class tạo rất nhiều object nhỏ, __slots__ cố định danh sách thuộc tính, bỏ __dict__:

slots.py

class KhongSlots:
    def __init__(self, x): self.x = x

class CoSlots:
    __slots__ = ("x",)          # co dinh thuoc tinh; bo __dict__
    def __init__(self, x): self.x = x

a = KhongSlots(1); print(hasattr(a, "__dict__"))   # True  (luu trong dict)
b = CoSlots(1);    print(hasattr(b, "__dict__"))   # False (tiet kiem hon)
b.y = 2            # AttributeError: khong co trong __slots__

Kết quả khi chạy

True
False
AttributeError
  • Mặc định thuộc tính ở __dict__ → đổi/thêm tuỳ ý nhưng tốn bộ nhớ.
  • __slots__: ít bộ nhớ & truy cập nhanh hơn, nhưng cố định danh sách thuộc tính.
  • Dùng __slots__ khi tạo HÀNG TRIỆU object nhỏ; bình thường không cần.

Tiếp theo

Bạn đã nắm OOP & mô hình dữ liệu (kể cả container, property, dataclass, slots). Bài kế tiếp đi sâu vào hai dunder quan trọng nhất: __iter__/__next__ - và generator (yield), thứ nằm ngay trong bản chất của vòng for.

Câu hỏi thường gặp

self là CHÍNH object đang gọi phương thức. Khi bạn viết p.move(), Python gọi Diem.move(p) - truyền p vào tham số đầu (tên quy ước là self). Nhờ self, phương thức biết nó đang làm việc trên object NÀO và đọc/ghi thuộc tính của object đó.

Dunder = “double underscore”, các phương thức tên dạng __ten__. Mô hình dữ liệu của Python là bộ quy ước: cú pháp & hàm dựng sẵn THỰC RA gọi dunder. len(x)x.__len__(); a + ba.__add__(b); x[i]x.__getitem__(i); item in xx.__contains__(item). Định nghĩa dunder = dạy object của bạn cư xử như kiểu dựng sẵn.

Định nghĩa giao thức container: __len__ cho len(); __getitem__ cho x[i] (và for tự suy ra nếu thiếu __iter__); __contains__ cho “x in obj”; __iter__ cho vòng for chuẩn (nên có để rõ ràng & nhanh). Viết đủ thì object “của bạn” hành xử y như list/dict dựng sẵn.

Cho phép GÁN như thuộc tính (obj.sodu = 50) nhưng chạy một hàm để KIỂM SOÁT (validate, chuẩn hoá, hoặc tính phái sinh). Vd setter từ chối số dư âm bằng raise ValueError. Lợi lớn: đổi từ thuộc tính thường sang có-logic mà KHÔNG phải sửa mã nơi dùng.

frozen=True → object BẤT BIẾN (gán lại báo FrozenInstanceError, lại hashable). order=True → tự sinh __lt__/__le__… để so sánh/sắp xếp. field(default_factory=list) → mỗi object một list MỚI (tránh bẫy mặc định mutable). __post_init__ → chạy thêm sau __init__ để kiểm tra/tính toán.

Mặc định mỗi object lưu thuộc tính trong một dict (__dict__) - linh hoạt nhưng tốn bộ nhớ & chậm hơn chút. Khai báo __slots__ = ("x", "y") cố định danh sách thuộc tính: bỏ __dict__ → ÍT bộ nhớ & truy cập nhanh hơn, nhưng không thêm thuộc tính ngoài danh sách được. Dùng khi tạo RẤT NHIỀU object nhỏ.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Khi viết d.tang(), Python thực chất làm gì với tham số self?

  1. 1

    Lớp TaiKhoan

    Viết class TaiKhoan@property sodu với setter từ chối số âm (raise ValueError), nap()/rut(), và __repr__ đẹp.

    Hoàn thành khi: tk.sodu = -1ValueError; rút quá số dư bị từ chối; print(tk) dễ đọc.

  2. 2

    Object “container”

    Viết class Tui cài __len__, __getitem__, __contains__ (và __iter__). Kiểm len(t), t[0], "x" in t, for x in t.

    Hoàn thành khi: Object của bạn dùng được như list: len/[]/in/for đều chạy đúng.

  3. 3

    Dunder cho Diem

    Dùng widget Bước 2: dự đoán p1 + p3p1 == p2 khi CÓ và KHÔNG có dunder, rồi kiểm bằng code thật.

    Hoàn thành khi: __add__Diem(4,6); không có → TypeError; có __eq__True; không có → False.

  4. 4

    dataclass bất biến

    Viết @dataclass(frozen=True, order=True) cho Diem; thử gán lại x; thử sorted một list Diem.

    Hoàn thành khi: Gán lại → FrozenInstanceError; sorted hoạt động nhờ order=True.

  5. 5

    Bẫy mặc định & __slots__

    (a) Vì sao field(default_factory=list) thay vì = []? (b) Thêm __slots__ cho một class rồi thử gán thuộc tính ngoài danh sách.

    Hoàn thành khi: (a) tránh dùng chung một list cho mọi object; (b) gán ngoài __slots__AttributeError (không còn __dict__).