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:
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__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__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__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__repr() cho str “kỹ thuật” để lập trình viên đọc - lý tưởng là tái tạo được object.
- ▸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__
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
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 + b → a.__add__(b); x[i] → x.__getitem__(i); item in x → x.__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.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Khi viết d.tang(), Python thực chất làm gì với tham số self?
- 1
Lớp TaiKhoan
Viết class
TaiKhoancó@propertysoduvới setter từ chối số âm (raise ValueError),nap()/rut(), và__repr__đẹp.Hoàn thành khi:
tk.sodu = -1→ValueError; rút quá số dư bị từ chối;print(tk)dễ đọc. - 2
Object “container”
Viết class
Tuicài__len__,__getitem__,__contains__(và__iter__). Kiểmlen(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
Dunder cho Diem
Dùng widget Bước 2: dự đoán
p1 + p3vàp1 == p2khi CÓ và KHÔNG có dunder, rồi kiểm bằng code thật.Hoàn thành khi: Có
__add__→Diem(4,6); không có →TypeError; có__eq__→True; không có →False. - 4
dataclass bất biến
Viết
@dataclass(frozen=True, order=True)choDiem; thử gán lạix; thửsortedmột listDiem.Hoàn thành khi: Gán lại →
FrozenInstanceError;sortedhoạt động nhờorder=True. - 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__).