- Khai báo
classvà__init__— hiểu vai trò củaself. - Phân biệt instance attribute và class attribute; tránh bẫy mutable class attribute.
- Sử dụng đúng
@classmethod,@staticmethod, instance method. - Master inheritance và
super(); hiểu MRO C3 linearization với multiple inheritance. - Implement dunder method:
__repr__,__eq__,__hash__,__len__,__iter__,__getitem__,__call__. - Dùng
@propertycho computed attribute và validation. - Dùng
@dataclassđể giảm boilerplate; biết khi nàofrozen=True,slots=True. - Phân biệt
abc.ABC(nominal subtype) vàtyping.Protocol(structural subtype). - Tư duy duck typing + EAFP thay vì
isinstancecheck khắp nơi.
Although practicality beats purity.
1. class — cú pháp tạo class
Class trong Python được khai báo bằng từ khóa class, theo sau là tên class (convention
PascalCase) và dấu hai chấm. Body của class là một namespace — chứa attribute và
method. Một class rỗng nhất có thể chỉ cần một dòng với pass.
class User:
pass
# Tạo instance — KHÔNG có new keyword
u = User()
print(type(u)) # <class '__main__.User'>
print(isinstance(u, User)) # True
# Có thể gán attribute động vào instance (giống JS)
u.name = "Anh"
print(u.name) # 'Anh'
JS: class User {}, tạo instance qua new User(). JS class là syntactic sugar trên prototype.
Dart: class User {}, tạo instance User() (Dart 2+ bỏ new bắt buộc); class là first-class type với compile-time check.
Python: User() — không có new. Class chính là callable; gọi class trả về instance. Class là một object thực sự (instance của type).
Vì class chỉ là namespace, có thể tham chiếu class như một giá trị bình thường: gán biến, truyền vào hàm,
trả về từ hàm. "Everything is an object" — đây là một câu thường được trích từ cộng đồng Python; kể
cả class cũng là object (instance của metaclass type).
class User:
pass
print(type(User)) # <class 'type'> — class là instance của type
print(isinstance(User, type)) # True
# Truyền class như giá trị
factories = [User, dict, list]
instances = [f() for f in factories]
print(instances) # [<User ...>, {}, []]
2. __init__ — constructor và self
__init__ là initializer được Python gọi ngay sau khi tạo instance.
Tham số đầu tiên luôn là self — tham chiếu tới instance vừa tạo. self không phải
từ khóa; nó chỉ là convention, nhưng tuyệt đối đừng đổi tên (vi phạm PEP 8 và làm code khó đọc).
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def greet(self) -> str:
return f"Xin chào, tôi là {self.name}"
u = User("Anh", 22)
print(u.greet()) # 'Xin chào, tôi là Anh'
self tự pass
Khi viết u.greet(), Python ngầm dịch thành User.greet(u) — instance được pass
như tham số đầu tiên. Đây gọi là bound method. Vì vậy hàm trong class luôn cần self
ở vị trí đầu. Nếu quên: TypeError: greet() takes 0 positional arguments but 1 was given.
__init__ không phải là người tạo ra object. Việc tạo instance thực ra do
__new__ đảm nhiệm (rất hiếm khi cần ghi đè). __init__ chỉ khởi tạo state lên
object đã có. Đó là lý do __init__ không có return (thực ra return được nhưng phải
là None).
Type hints trong __init__
Từ Python 3.5+, có thể annotate kiểu cho tham số và return. __init__ trả None
nên thường bỏ annotation return. Type hints không bị runtime enforce — chỉ tooling (mypy, IDE) kiểm tra.
Sẽ học sâu ở Chương 9.
3. Instance attribute vs class attribute
Có hai chỗ để gán attribute:
- Instance attribute:
self.x = ...trong method (thường ở__init__). Mỗi instance có bản sao riêng. - Class attribute: khai báo trực tiếp trong body class, ngoài method. Một bản chia sẻ giữa mọi instance.
class User:
species = "human" # class attribute — share
count = 0 # class attribute
def __init__(self, name):
self.name = name # instance attribute — riêng từng instance
User.count += 1 # cập nhật class attr qua tên class
a = User("Anh")
b = User("Bình")
print(a.species, b.species) # human human — chia sẻ
print(User.count) # 2
print(a.name, b.name) # Anh Bình — riêng
Đừng bao giờ dùng list, dict, set làm class
attribute mặc định cho dữ liệu instance. Vì class attribute chia sẻ — mọi instance sẽ mutate cùng một
list!
# SAI
class Cart:
items = [] # class attr — chia sẻ!
def add(self, x):
self.items.append(x) # mutate list chung
c1, c2 = Cart(), Cart()
c1.add("A")
print(c2.items) # ['A'] — Cart() khác cũng thấy!
# ĐÚNG
class Cart:
def __init__(self):
self.items = [] # instance attr — list riêng
Khi đọc a.x, Python tra cứu theo thứ tự: instance dict → class dict → MRO của các lớp cha. Vì
vậy class attribute hoạt động như fallback nếu instance không có. Một khi gán a.x = ...,
giá trị mới được tạo trong instance dict và che lấp class attribute (không ảnh hưởng class).
4. Ba loại method — instance / class / static
Python phân biệt 3 loại method qua decorator. Sự khác biệt nằm ở tham số đầu tiên tự pass:
| Loại | Decorator | Tham số tự pass | Truy cập gì | Khi nào dùng |
|---|---|---|---|---|
| Instance method | (không cần) | self (instance) |
Instance + class attr | Hành vi của 1 instance |
| Class method | @classmethod |
cls (class) |
Class attr; tạo instance thay thế | Alternative constructor, factory |
| Static method | @staticmethod |
(không có) | Không tự pass; chỉ là namespace | Utility liên quan logic của class |
from __future__ import annotations
from datetime import date
class User:
def __init__(self, name: str, birth_year: int):
self.name = name
self.birth_year = birth_year
# Instance method — dùng self
def age(self) -> int:
return date.today().year - self.birth_year
# Class method — alternative constructor
@classmethod
def from_dict(cls, data: dict) -> User:
return cls(name=data["name"], birth_year=data["birth_year"])
# Static method — utility
@staticmethod
def is_adult_age(age: int) -> bool:
return age >= 18
u = User.from_dict({"name": "Anh", "birth_year": 2003})
print(u.age()) # 23
print(User.is_adult_age(17)) # False
@classmethod ưu việt với inheritance
cls trong @classmethod là class thực tế được gọi — không phải
class khai báo. Khi Admin kế thừa User và gọi Admin.from_dict(...),
cls chính là Admin, nên kết quả là một instance Admin. Nếu hard-code
User(...) bên trong, subclass sẽ không được tận dụng.
5. Inheritance — class Admin(User)
Để kế thừa, đặt class cha trong dấu ngoặc: class Admin(User):. Subclass tự động thừa hưởng mọi
method và attribute của parent. Override = định nghĩa lại method cùng tên trong subclass. Để gọi
implementation của cha (ví dụ trong __init__ của subclass), dùng super().
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hi, {self.name}"
class Admin(User):
def __init__(self, name, level):
super().__init__(name) # chain init lên cha
self.level = level
def greet(self): # override
base = super().greet() # gọi version của cha
return f"{base} [admin lv {self.level}]"
a = Admin("Anh", 2)
print(a.greet()) # 'Hi, Anh [admin lv 2]'
print(isinstance(a, User)) # True
Không cần truyền self hay class hiện tại vào super() — Python tự suy ra từ context.
Trong Python 2, phải viết super(Admin, self).__init__(name), nhưng Python 3 đã đơn giản hóa.
JS: class Admin extends User { constructor(name, level) { super(name); this.level = level; } }. Bắt buộc gọi super() trước khi đụng this.
Dart: class Admin extends User { Admin(super.name, this.level); } — Dart 2.17+ có super-parameter shorthand rất gọn.
Python: super().__init__(...) không bắt buộc; quên thì instance không có attribute mà parent gán. Tự do hơn nhưng cũng dễ sai hơn.
6. Multiple inheritance và MRO C3
Python cho phép kế thừa nhiều class: class C(A, B):. Khi tra cứu method, Python đi theo
MRO (Method Resolution Order) — thứ tự duyệt class. Thuật toán dùng là C3
linearization, đảm bảo: (a) class con trước class cha; (b) thứ tự khai báo bên trái sang phải;
(c) tính nhất quán (monotonic).
class A:
def who(self): return "A"
class B:
def who(self): return "B"
class C(A, B):
pass
print(C().who()) # 'A' — A đứng trước B trong khai báo
print([k.__name__ for k in C.__mro__])
# ['C', 'A', 'B', 'object']
MRO trở nên thú vị (và đôi khi bối rối) với diamond inheritance — hai class cha chia sẻ một ông nội. C3 đảm bảo class ông nội chỉ xuất hiện một lần ở cuối.
class Animal:
def describe(self): return "animal"
class Swimmer(Animal):
def describe(self): return "swimmer-" + super().describe()
class Flyer(Animal):
def describe(self): return "flyer-" + super().describe()
class Duck(Swimmer, Flyer):
pass
print(Duck.__mro__)
# (Duck, Swimmer, Flyer, Animal, object)
print(Duck().describe())
# 'swimmer-flyer-animal' — super() đi đúng theo MRO
super() không phải "cha trực tiếp"
Điểm khác biệt then chốt: super() đi tới class kế tiếp trong MRO, không phải
"parent của class hiện tại". Trong ví dụ Duck, super() bên trong
Swimmer.describe trỏ sang Flyer — vì MRO sắp xếp Flyer ngay sau
Swimmer. Đây là điều kỳ diệu của C3: chain super() xếp hàng đẹp với diamond.
Nếu C3 không tìm được thứ tự nhất quán, Python từ chối khai báo class và ném
TypeError: Cannot create a consistent method resolution order. Trường hợp đó thường do thứ tự
base class mâu thuẫn — cần đổi lại thứ tự khai báo.
7. Dunder method — "double underscore" magic
Dunder method (đọc là "dander", viết tắt "double underscore") là các method tên kiểu __xxx__
mà Python tự gọi khi gặp syntax tương ứng. Override dunder = cho phép class của bạn hòa nhập với syntax ngôn
ngữ: +, ==, len(), for ... in, print()...
| Dunder | Trigger | Mục đích |
|---|---|---|
__init__ | Sau khi tạo instance | Khởi tạo state |
__repr__ | repr(obj), REPL echo | Debug-friendly, eval-able nếu có thể |
__str__ | str(obj), print(obj), f-string | User-facing display |
__eq__ | a == b | So sánh giá trị (mặc định: so sánh id) |
__hash__ | hash(obj), key của dict/set | Cùng đi với __eq__ |
__lt__, __le__, __gt__, __ge__ | < <= > >= | So sánh thứ tự (xem functools.total_ordering) |
__len__ | len(obj) | Số phần tử; cũng xác định truthiness nếu không có __bool__ |
__bool__ | bool(obj), if obj: | Truthiness |
__iter__ | for x in obj | Trả iterator (xem Chương 6) |
__next__ | next(it) | Iterator protocol |
__contains__ | x in obj | Membership test |
__getitem__ | obj[key] | Indexing/slicing |
__setitem__ | obj[key] = value | Gán index |
__delitem__ | del obj[key] | Xóa index |
__call__ | obj(...) | Cho phép instance "gọi như hàm" |
__add__, __sub__, __mul__... | a + b, a - b... | Toán tử số học |
__enter__, __exit__ | with obj as x: | Context manager (Chương 7) |
7.1. __repr__ vs __str__
Convention quan trọng: __repr__ dành cho developer — nên hiển thị chi tiết, tốt nhất
là một biểu thức Python hợp lệ có thể eval() lại thành object tương đương. __str__
dành cho người dùng cuối — gọn, đẹp. Nếu chỉ định nghĩa __repr__, Python tự lấy nó làm
__str__.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(1, 2)
print(repr(p)) # Point(x=1, y=2)
print(str(p)) # (1, 2)
print(p) # (1, 2) — print gọi __str__
7.2. __eq__ và __hash__ — luôn đi cùng nhau
Mặc định, hai instance khác nhau không bằng nhau dù state giống hệt — Python so sánh
bằng id (địa chỉ object). Để so sánh theo giá trị, override __eq__. Nhưng có một quy tắc bắt
buộc:
__eq__ ⇒ __hash__
Nếu override __eq__ nhưng không định nghĩa __hash__, Python tự đặt
__hash__ = None — instance trở thành unhashable, không bỏ vào set
hay làm key dict được. Quy tắc bất di bất dịch của hash: a == b ⇒ hash(a) == hash(b).
Ngược lại không bắt buộc (collision được phép).
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # tuple đã hashable
a, b = Point(1, 2), Point(1, 2)
print(a == b) # True
print({a, b}) # {Point(x=1, y=2)} — set giữ 1 phần tử
Trả về NotImplemented (không phải NotImplementedError!) trong __eq__
khi không biết cách so sánh với kiểu khác — Python sẽ thử other.__eq__(self), rất quan trọng để
hỗ trợ subclass và so sánh hai chiều.
7.3. __iter__, __contains__, __getitem__
class Deck:
def __init__(self):
self.cards = ["A♠", "K♠", "Q♠", "J♠"]
def __len__(self):
return len(self.cards)
def __getitem__(self, i):
return self.cards[i]
def __contains__(self, card):
return card in self.cards
d = Deck()
print(len(d)) # 4
print(d[0]) # 'A♠'
print("A♠" in d) # True
for c in d: # for-loop tự dùng __getitem__ nếu không có __iter__
print(c)
Nếu có __getitem__ chấp nhận số nguyên 0, 1, 2... Python tự coi object là iterable (legacy
protocol). Tuy nhiên, cách "Pythonic" hiện đại là implement __iter__ — sâu hơn ở
Chương 6 (Iterators & Generators).
7.4. __call__ — instance như là hàm
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
print(double(5)) # 10 — instance gọi như hàm
print(callable(double)) # True
__call__ rất hữu ích cho stateful callable — như decorator có config, hàm tính có
cache, partial function. Đó cũng là lý do functools.partial trả về một object có
__call__.
8. @property — method behave như attribute
@property cho phép truy cập một method không cần dấu ngoặc () — y hệt
attribute. Dùng cho computed value (giá trị tính từ field khác) và validation
(kiểm tra khi gán).
class Circle:
def __init__(self, radius):
self.radius = radius # dùng setter nếu có
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("radius phải >= 0")
self._radius = value
@property
def area(self):
from math import pi
return pi * self._radius ** 2
c = Circle(3)
print(c.radius) # 3 — không có ()
print(c.area) # 28.274... — computed
c.radius = 5 # setter chạy validation
# c.radius = -1 → ValueError
Nếu chỉ có @property không có setter — attribute trở thành read-only. Có thể
thêm @xxx.deleter để custom del obj.xxx.
JS: get area() {} / set radius(value) {} trong class.
Dart: double get area => pi * _radius * _radius; — getter cú pháp riêng, gọn.
Python: dùng decorator @property + @xxx.setter. Hơi dài dòng nhưng nhất quán với hệ thống decorator chung của ngôn ngữ.
9. @dataclass — giảm boilerplate
Mỗi khi viết class để giữ dữ liệu, ta phải gõ lại __init__, __repr__,
__eq__ giống nhau. Python 3.7 đưa ra @dataclass — decorator generate những method
này tự động dựa trên type annotation của class.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str = "" # default value
u1 = User("Anh", 22)
u2 = User("Anh", 22)
print(u1) # User(name='Anh', age=22, email='')
print(u1 == u2) # True — __eq__ auto theo field
Mặc định @dataclass tạo: __init__, __repr__, __eq__.
Các tham số đáng nhớ:
| Tham số | Mặc định | Hiệu ứng |
|---|---|---|
init=True | True | Generate __init__ |
repr=True | True | Generate __repr__ |
eq=True | True | Generate __eq__ |
order=True | False | Generate __lt__...__ge__ |
frozen=True | False | Instance bất biến + auto __hash__ |
slots=True | False (3.10+) | Generate __slots__, tiết kiệm RAM, cấm attr mới |
kw_only=True | False (3.10+) | Mọi tham số là keyword-only |
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print({p}) # {Point(x=1.0, y=2.0)} — hashable vì frozen
# p.x = 9 → FrozenInstanceError
# p.z = 0 → AttributeError (slots không cho thêm attr)
Không được viết items: list = [] vì sẽ chia sẻ list giữa các instance (giống bẫy mutable
class attribute). Dùng field(default_factory=list):
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list[str] = field(default_factory=list)
discount: float = 0.0
10. Abstract Base Class — abc
Khi muốn ép subclass phải implement một method nhất định, dùng abc.ABC + decorator
@abstractmethod. Class có @abstractmethod không thể tạo instance trực tiếp.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
# Shape() → TypeError: Can't instantiate abstract class Shape
print(Square(3).area()) # 9
ABC tương đương interface ở các ngôn ngữ khác — nhưng vẫn được implement bằng class, nên có thể
chứa method có sẵn (concrete method) lẫn @abstractmethod. Đó là lý do thường được gọi là
"abstract base class" thay vì "interface": vừa làm khung vừa cung cấp logic chung.
11. typing.Protocol — structural typing
PEP 544 (Python 3.8+) đưa ra typing.Protocol — cho phép khai báo "type theo cấu trúc": "bất kỳ
class nào có method X, Y với signature đúng đều coi là SupportsX". Không cần inherit
— đây là structural typing, giống interface trong TypeScript / Go.
from typing import Protocol
class SupportsArea(Protocol):
def area(self) -> float: ...
class Box: # không inherit SupportsArea
def __init__(self, w, h):
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
def describe(s: SupportsArea) -> str:
return f"area = {s.area()}"
print(describe(Box(2, 3))) # 'area = 6' — mypy chấp nhận
ABC (nominal): "Bạn phải khai báo bạn là Shape" — phải class Box(Shape).
Quan hệ tường minh, kiểm tra được lúc runtime qua isinstance.
Protocol (structural): "Nếu trông giống Shape, làm việc giống Shape, thì bạn là Shape" —
không cần khai báo. Chỉ là contract về cấu trúc, chủ yếu phục vụ tooling (mypy). Thường gọn hơn cho API
"duck-typed".
Chương 9 (Typing) sẽ đào sâu Protocol + Generic + runtime_checkable.
12. Duck typing — "Nếu kêu quack quack thì là vịt"
Câu thành ngữ nguyên gốc: "If it walks like a duck and it quacks like a duck, then it must be a duck." Áp dụng vào Python: ta thường không quan tâm đối tượng là class gì, chỉ quan tâm nó có method/attribute cần thiết hay không. Đó là duck typing.
def play_sound(animal):
# Không quan tâm animal là class gì, chỉ cần có .speak()
return animal.speak()
class Duck:
def speak(self): return "quack"
class Robot:
def speak(self): return "beep"
print(play_sound(Duck())) # 'quack'
print(play_sound(Robot())) # 'beep' — Robot không phải Duck nhưng OK
Cộng đồng Python đề cao EAFP ("Easier to Ask Forgiveness than Permission") — cứ làm, sai thì bắt exception. Đối lập với LBYL ("Look Before You Leap") — kiểm tra trước rồi mới làm.
# LBYL — không "Pythonic"
if hasattr(obj, "speak") and callable(obj.speak):
obj.speak()
# EAFP — Pythonic
try:
obj.speak()
except AttributeError:
handle_no_speak()
Khi nào dùng isinstance? Khi thật sự cần ép contract (vd API public, kiểm tra để
hỗ trợ kiểu mới một cách dạng "double dispatch"). Còn lại — tin vào duck typing.
Unless explicitly silenced.
Bài tập
Vector 2D với toán tử
Viết class Vector2D với hai thuộc tính x, y. Implement các
dunder method: __init__, __repr__, __eq__,
__add__, __sub__, __mul__ (scalar bên phải). Yêu cầu:
Vector2D(1, 2) + Vector2D(3, 4) == Vector2D(4, 6)Vector2D(3, 4) - Vector2D(1, 2) == Vector2D(2, 2)Vector2D(1, 2) * 3 == Vector2D(3, 6)repr(Vector2D(1, 2))phải eval lại được
Gợi ý
Trong các toán tử __add__/__sub__, trả về một instance mới — không
mutate self. __mul__(self, scalar) nhận một số làm tham số thứ hai. Khi
so sánh kiểu khác, trả NotImplemented. Để ổn định repr, dùng
f"Vector2D({self.x}, {self.y})".
Hierarchy Shape với ABC
Tạo class trừu tượng Shape (kế thừa abc.ABC) với
@abstractmethod area(). Implement Circle(radius), Square(side),
Rectangle(width, height). Sau đó:
- Tạo list
shapes = [Circle(2), Square(3), Rectangle(2, 5)] - In ra tổng diện tích bằng
sum(s.area() for s in shapes) - Thử
Shape()→ phải raTypeError
Gợi ý
area() ở Shape chỉ cần ... (Ellipsis literal) làm body.
Mỗi subclass override area() với công thức riêng. Đây là ví dụ kinh điển của
polymorphism — code dùng list không cần biết từng phần tử là Circle hay Square.
Refactor User sang @dataclass(frozen=True)
Cho class User dạng "manual":
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
def __eq__(self, other):
if not isinstance(other, User): return NotImplemented
return (self.name, self.age) == (other.name, other.age)
def __hash__(self):
return hash((self.name, self.age))
Viết lại dưới dạng @dataclass(frozen=True). So sánh số dòng. Kiểm tra:
hash(User("A", 1)) hoạt động, User("A", 1) == User("A", 1) trả
True, và user.name = "B" ném FrozenInstanceError.
Gợi ý
frozen=True đồng thời cấm set attribute và bật eq=True
(mặc định) sẽ kéo theo auto-generate __hash__. Phiên bản sau gọn còn ~4 dòng.
Lớp Temperature đa đơn vị
Tạo class Temperature lưu state trong _celsius (convention private).
Implement @property cho ba đơn vị: celsius, fahrenheit,
kelvin. Mỗi property có cả getter và setter — setter chuyển ngược về Celsius rồi cập nhật
_celsius.
- Công thức:
F = C × 9/5 + 32,K = C + 273.15 t = Temperature(); t.celsius = 100; print(t.fahrenheit, t.kelvin)→212.0 373.15t.fahrenheit = 32; print(t.celsius)→0.0
Gợi ý
Validation hợp lý: setter kelvin kiểm tra value < 0 →
ValueError("dưới 0 K không có nghĩa"). Mỗi setter cập nhật _celsius
duy nhất; getter các đơn vị khác đều derive từ _celsius.
Mixin Serializable
Tạo mixin Serializable với method to_json(self) trả về chuỗi JSON đại diện
cho instance. Dùng dataclasses.asdict để convert dataclass thành dict, rồi
json.dumps. Áp dụng lên dataclass Product:
from dataclasses import dataclass
@dataclass
class Product(Serializable):
sku: str
name: str
price: float
print(Product("P001", "Book", 19.99).to_json())
# {"sku": "P001", "name": "Book", "price": 19.99}
Vẽ Product.__mro__ và xác nhận thứ tự: Product → Serializable → object.
Gợi ý
Mixin là một class thông thường, không cần inherit ABC — chỉ chứa method công cộng để
được mix vào nhiều class khác qua multiple inheritance. Đặt mixin trước
object trong khai báo, nhưng theo convention nên đặt sau class chính nếu có
nhiều base.
Quiz
self là gì? Tại sao phải khai báo như tham số đầu?
Xem đáp án
self là tham chiếu tới instance mà method đang được gọi trên. Khi viết obj.method(x),
Python ngầm dịch thành Class.method(obj, x) — instance được truyền vào tham số đầu. Vì vậy
method instance bắt buộc nhận self. Cái tên "self" chỉ là convention (PEP 8); thay đổi sẽ
vẫn chạy được nhưng làm code khó đọc và bị linter cảnh báo.
@classmethod và @staticmethod khác nhau ra sao?
Xem đáp án
@classmethod nhận cls tự động — là class thực tế được gọi (subclass
sẽ thấy class mình). Thường dùng làm alternative constructor (from_dict,
from_file...). @staticmethod không nhận tham số tự động nào — chỉ là utility
được đặt trong namespace class cho gọn. Vì không có cls, static method không hỗ trợ
polymorphism qua class. Khi cần override theo subclass, chọn @classmethod.
__str__ và __repr__ khác gì? Khi nào ta cần cả hai?
Xem đáp án
__str__ dành cho người dùng cuối — gọn, đẹp, dễ đọc (vd
"(1, 2)"). __repr__ dành cho developer — chi tiết, mục tiêu
lý tưởng là eval(repr(obj)) trả về object tương đương (vd "Point(x=1, y=2)").
Nếu chỉ định nghĩa __repr__, Python tự lấy nó làm __str__ — nên một class
tối thiểu chỉ cần __repr__. Override cả hai khi muốn UX hiển thị khác (vd log internal
chi tiết hơn cho debug).
Với class C(A, B), Python quyết định thứ tự lookup method ra sao?
Xem đáp án
Python dùng thuật toán C3 linearization để tính MRO (Method Resolution Order). Quy
tắc: (1) class con trước class cha, (2) bên trái trước bên phải trong khai báo, (3) thứ tự nhất quán
giữa các MRO. Có thể xem qua C.__mro__ hoặc C.mro(). super() đi
theo MRO chứ không phải "cha trực tiếp" — đó là chìa khóa làm cho diamond inheritance hoạt động đúng.
@dataclass tự generate những method nào? Khi nào có __hash__?
Xem đáp án
Mặc định: __init__, __repr__, __eq__. Option order=True
bổ sung __lt__...__ge__. __hash__ được generate khi
frozen=True (cùng với eq=True, đây là mặc định). Nếu chỉ có
eq=True mà không frozen=True, dataclass đặt __hash__ = None →
instance unhashable. Có thể ép unsafe_hash=True nhưng rủi ro (instance mutable mà hashable
→ hỏng dict/set).
Setter của @property có bắt buộc không?
Xem đáp án
Không bắt buộc. Nếu chỉ có @property mà không có @xxx.setter, attribute
trở thành read-only — gán obj.xxx = ... sẽ ném
AttributeError: can't set attribute. Đây là cách phổ biến để khai báo computed
property (vd area tính từ radius) hoặc immutable field.
Có thể thêm @xxx.deleter để custom del obj.xxx.
Khi override __eq__, ta còn phải override gì? Tại sao?
Xem đáp án
Phải override __hash__. Lý do: Python tự đặt __hash__ = None khi
phát hiện __eq__ custom — instance sẽ unhashable. Quy tắc hash bất di bất dịch:
a == b ⇒ hash(a) == hash(b). Nếu chỉ override __eq__ mà không cập nhật hash,
hai instance "bằng nhau" lại có hash khác → vi phạm hợp đồng của set/dict,
dẫn đến bug khủng khiếp. Quy tắc đơn giản: object mutable (có thể đổi state) → đừng hashable; object
immutable → hash từ một tuple các field.
Duck typing vs isinstance check — cái nào "Pythonic" hơn?
Xem đáp án
Trong đa số trường hợp, duck typing Pythonic hơn — kết hợp với phong cách
EAFP (cứ làm, exception thì bắt). Code linh hoạt, dễ test (mock object chỉ cần expose
đúng method), không khóa kiểu cụ thể.
isinstance hợp lý khi: (a) thực sự cần phân biệt nhiều kiểu khác nhau (vd hỗ trợ
str và list input cho cùng API); (b) viết API public cần guarantee mạnh;
(c) dùng typing.Protocol với @runtime_checkable để kết hợp ưu điểm cả hai —
có structural type vẫn check được runtime. Tránh isinstance theo thói quen ép kiểu;
tin vào hành vi của object.
Tổng kết
Sau chương 5, bạn đã master:
- Class &
__init__: cú pháp,self, instance vs class attribute, bẫy mutable. - Ba loại method: instance,
@classmethod(alternative constructor),@staticmethod(namespace utility). - Inheritance:
class B(A), override,super().__init__chain. - Multiple inheritance & MRO: C3 linearization,
__mro__, diamond. - Dunder methods:
__repr__/__str__,__eq__/__hash__luôn đi đôi,__len__,__iter__,__contains__,__getitem__,__call__. @property: computed value, validation, read-only attribute.@dataclass:frozen,slots,field(default_factory=...).- ABC vs Protocol: nominal vs structural typing; duck typing & EAFP làm tinh thần cốt lõi.
Kết nối
- Chương 6 (Iterators & Generators) — đào sâu
__iter__/__next__, iterable protocol vàyieldđể tạo class lazy. - Chương 7 (Decorators & Context Managers) —
@functools.wrapsbảo toàn metadata khi decorator class method;__enter__/__exit__chowithstatement. - Chương 9 (Typing & Static Analysis) —
Protocolđi vớiGeneric,TypedDict, type narrowing quaisinstance; PEP 695 Generic syntax (3.12+). - JavaScript Chương 4 (Objects, Prototypes & Classes) — đối chiếu mô hình prototype của JS với class-as-namespace của Python.
- Dart Chương 4 (Class Modifiers) — Dart 3 đưa ra
sealed/base/final/interface class, đối lập với phong cách tự do của Python (không có private thật sự, không có sealed).