Câu 1Khi result = a + b chạy, Python gọi method nào ngầm bên dưới?
Special method — Dạy class của bạn cách + và print hành xử
Học special method (dunder method) của Python. Tùy biến + với __add__, kiểm soát đầu ra của print với __str__, và định nghĩa lại == với __eq__ — kèm sơ đồ.
Lần trước ta đã sắp xếp instance, class và static method. Lần này ta sẽ bao quát một loại khác — special method (còn gọi là dunder method).
Special method là gì? — "Dunder method"
Các phương thức như __init__ và __str__ — tên được bao quanh bởi hai dấu gạch dưới ở mỗi bên — được gọi là special method. Vì chúng nằm giữa double underscores, chúng còn được gọi là dunder method.
Điều quan trọng về special method là bạn không gọi chúng trực tiếp. Python tự động gọi chúng khi bạn thực hiện một số thao tác nhất định. Viết v1 + v2 và Python gọi v1.__add__(v2) ngầm bên dưới. Viết print(p) thì p.__str__() được gọi. Viết a == b thì a.__eq__(b) chạy.
+, ==, và bạn bè.__add__ — Định nghĩa toán tử +
Lấy + làm ví dụ rõ ràng. Giả sử bạn có class Money cho số tiền tính bằng yen, và bạn muốn Money(300) + Money(500) cho ra Money(800). Cứ thế cộng và Python sẽ ném ra TypeError phàn nàn rằng nó không biết cách cộng Money với Money.
Cách dạy Python cộng chúng là phương thức __add__. Định nghĩa def __add__(self, other): và trả về một instance mới được dựng từ self (vế trái) và other (vế phải). Giờ + hoạt động trên class của bạn.
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other): # được gọi khi viết +
return Money(self.amount + other.amount)
wallet = Money(300)
payment = Money(500)
total = wallet + payment # thực ra là wallet.__add__(payment)
print(total.amount) # 800
+ và Python gọi self.__add__(other). self là toán hạng trái, other là toán hạng phải. Bất cứ instance mới nào bạn trả về sẽ trở thành kết quả của +.__str__ và __repr__ — Hai loại biểu diễn chuỗi
Tiếp theo: special method được gọi khi một instance được chuyển thành chuỗi. Có hai cái, với mục tiêu khác nhau.
- __str__ — được gọi bởi print(p) và str(p). Một chuỗi thân thiện với người dùng, dễ đọc.
- __repr__ — được gọi trong REPL hoặc cho debug. Một chuỗi giống code, chi tiết.
Không có override, print(user) cho ra cái gì đó như <__main__.User object at 0x...> — khá vô dụng. Định nghĩa __str__ cho hiển thị gọn gàng, và định nghĩa cả __repr__ để debug cho bạn thấy kiểu và nội dung trong nháy mắt.
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self): # cho print() / str()
return f"{self.name} ({self.age} tuổi)"
def __repr__(self): # cho debug
return f"User(name={self.name!r}, age={self.age})"
u = User("Minh", 30)
print(u) # Minh (30 tuổi) <- __str__
print(repr(u)) # User(name='Minh', age=30) <- __repr__
- Được gọi bởi
print(u)vàstr(u) - Mục tiêu: chuỗi cho người dùng cuối đọc
- Ví dụ:
Minh (30 tuổi)
- Được gọi bởi
repr(u)và REPL tương tác - Quay về đây khi __str__ thiếu trong lúc print
- Mục tiêu: chuỗi debug hiển thị kiểu và nội dung
- Ví dụ:
User(name='Minh', age=30)
__repr__ — nó làm việc debug dễ hơn nhiều.__eq__ — Định nghĩa ==
Giờ đến đẳng thức. Khi bạn viết a == b, Python gọi a.__eq__(b) ngầm bên dưới. Nếu class của bạn không định nghĩa __eq__, mặc định là "chúng có phải cùng object trong bộ nhớ không?" (so sánh id).
Giả sử bạn có class Coupon và bạn muốn hai coupon có cùng code được tính là cùng coupon — kể cả khi giá trị discount khác nhau. Đó là một đẳng thức ở "mức nghiệp vụ", và __eq__ là nơi bạn nói rõ nó.
== là kiểm tra id — cùng nội dung nhưng instance riêng biệt vẫn ra False. Có __eq__, bạn có thể so sánh theo nội dung (code).class Coupon:
def __init__(self, code, discount):
self.code = code
self.discount = discount
def __eq__(self, other):
return self.code == other.code # cùng code = cùng coupon
c1 = Coupon("SPRING10", 0.10)
c2 = Coupon("SPRING10", 0.20) # discount khác, code giống
c3 = Coupon("SUMMER15", 0.15)
print(c1 == c2) # True (code khớp)
print(c1 == c3) # False (code khác)
Điều gì xảy ra khi không có __eq__?
Nếu bạn không định nghĩa __eq__, c1 == c2 kiểm tra chúng có phải cùng object trong bộ nhớ không (theo nghĩa đen là chỉ tới cùng một hộp). Kể cả khi mọi thuộc tính khớp hoàn hảo, hai instance được dựng riêng biệt sống ở các địa chỉ bộ nhớ khác nhau và kết quả là False. Bất cứ khi nào bạn muốn "bằng theo nội dung", hãy tự định nghĩa __eq__.
Special method phổ biến khác — __call__ / __len__ / __bool__
Ngoài bốn cái ta đã bao quát, vài special method khác xuất hiện thường xuyên. Ba cái đáng biết ngay:
- __call__ — làm cho một instance gọi được như một hàm (cú pháp obj(...))
- __len__ — định nghĩa len(obj) trả về cái gì
- __bool__ — định nghĩa instance đánh giá thế nào như một giá trị chân lý trong if obj: hoặc bool(obj)
Mỗi cái dạy một thao tác built-in — gọi hàm, len(), kiểm tra truthiness của if — cho class tùy chỉnh của bạn. Một Logger tích lũy log entry là ví dụ tuyệt vời để đặt cả ba lên một class.
class Logger:
def __init__(self, name):
self.name = name
self.log = []
def __call__(self, message): # logger("...") hoạt động
self.log.append(message)
return f"[{self.name}] {message}"
def __len__(self): # cho len(logger)
return len(self.log)
def __bool__(self): # cho if logger:
return len(self.log) > 0
app = Logger("app")
print(app("Ứng dụng đã khởi động")) # [app] Ứng dụng đã khởi động
print(app("Đăng nhập thành công")) # [app] Đăng nhập thành công
print(len(app)) # 2
if app:
print("Có log") # Có log
logger(...), len(logger), if logger: — được dạy cho class tùy chỉnh của bạn qua ba dunder tương ứng.Nếu bạn không định nghĩa __bool__ thì sao?
Khi __bool__ thiếu, Python quay về __len__. Độ dài 0 thành False; cái khác thành True. Nếu cả hai đều thiếu, instance luôn truthy, bất kể trạng thái. Đó cũng chính là quy tắc khiến list rỗng và chuỗi rỗng đánh giá thành False.
Còn nhiều nữa — __hash__ (để dùng làm key của set/dict), __lt__ / __gt__ (so sánh < / >), __getitem__ (cú pháp obj[key]), __iter__ (lặp for x in obj:), và những cái khác. Bạn không cần ghi nhớ hết. Chỉ giữ bản đồ mental này: "nếu bạn muốn dạy một thao tác built-in cho class, có lẽ có một dunder tương ứng cho nó." Rồi tra cứu khi cần.
| Special method | Cú pháp | Khi nào được gọi |
|---|---|---|
| __init__ | Money(300) | Khi tạo instance |
| __add__ | a + b | Toán tử + |
| __str__ | print(p) / str(p) | Chuỗi hướng người dùng |
| __repr__ | repr(p) / hiển thị REPL | Chuỗi hướng dev |
| __eq__ | a == b | So sánh đẳng thức |
| __call__ | obj(...) | Gọi kiểu hàm |
| __len__ | len(obj) | Độ dài |
| __bool__ | if obj: / bool(obj) | Truthiness |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Mô tả nào về __str__ và __repr__ là chính xác nhất?
Câu 3Nếu một class không định nghĩa __eq__, a == b so sánh cái gì? (Cả hai là instance của cùng class.)