Học bằng cách đọc theo thứ tự

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

Toán tử và special method tương ứng
Bạn viếtPythonchuyểnMethodđược gọiv1 + v2__add__print(p)__str__a == b__eq__
Toán tử và built-in function của Python gọi các dunder method tương ứng ngầm bên dưới. Định nghĩa chúng trên class của bạn và kiểu tự định nghĩa có thể dùng +, ==, 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):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
v1 + v2 làm gì ngầm bên dưới
wallet+ paymentwallet.__add__(payment)self =walletother =paymentreturn Money(self.amount + other.amount)trả vềMoney(800)chuyển
Viết + 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 +.

Hiện thực __add__ trên Money để + cộng hai số dư.

① Định nghĩa class Money: với __init__(self, amount) gán self.amount = amount.

② Định nghĩa __add__(self, other) trả về Money(self.amount + other.amount).

③ Tạo wallet = Money(300)payment = Money(500), rồi print total.amount với total = wallet + payment.

(Khi chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

__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)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__
Khi nào dùng __str__ vs __repr__
__str__ (hướng người dùng)
  • Được gọi bởi print(u)str(u)
  • Mục tiêu: chuỗi cho người dùng cuối đọc
  • Ví dụ: Minh (30 tuổi)
__repr__ (hướng dev)
  • Đượ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)
Định nghĩa cả hai là lý tưởng, nhưng nếu bạn chỉ định nghĩa một, hãy chọn __repr__ — nó làm việc debug dễ hơn nhiều.

Hiện thực __str____repr__ trên User và so sánh print với repr cho ra cái gì.

① Định nghĩa class User: với __init__(self, name, age) gán self.name / self.age.

② Định nghĩa __str__(self) trả về f"{self.name} ({self.age} tuổi)".

③ Định nghĩa __repr__(self) trả về f"User(name={self.name!r}, age={self.age})". !r sau self.name nghĩa là nhúng kết quả gọi repr() trên giá trị — chuỗi ra với dấu nháy đơn ('Minh') thay vì văn bản trần (Minh).

④ Tạo u = User("Minh", 30), rồi chạy cả print(u)print(repr(u)) để thấy sự khác biệt.

Python Editor

Chạy code để xem đầu ra

__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ó.

__eq__ thay đổi hành vi của == thế nào
c1 == c2không có__eq__kiểm tra id(địa chỉ)__eq__self.code== other.codemặc địnhđã định nghĩa
Không có __eq__, == 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__.

Hiện thực __eq__ trên Coupon để hai coupon bằng nhau khi code của chúng khớp.

① Định nghĩa class Coupon: với __init__(self, code, discount) gán self.codeself.discount.

② Định nghĩa __eq__(self, other) trả về self.code == other.code.

③ Tạo ba coupon — Coupon("SPRING10", 0.10), Coupon("SPRING10", 0.20), và Coupon("SUMMER15", 0.15) — và print hai phép so sánh ==.

Python Editor

Chạy code để xem đầu ra

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
Dạy Logger nói cú pháp built-in
app('msg')app.__call__('msg')len(app)app.__len__()if app:app.__bool__()trở thànhtrở thànhtrở thành
Cú pháp quen thuộc — 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.

Hiện thực ba special method trên Logger để thêm message, đếm chúng, và kiểm tra rỗng đều hoạt động với cú pháp built-in.

① Định nghĩa class Logger: với __init__(self, name) thiết lập self.name = nameself.log = [].

② Định nghĩa __call__(self, message): append message vào self.log, rồi trả về f"[{self.name}] {message}".

③ Định nghĩa __len__(self) trả về len(self.log).

④ Định nghĩa __bool__(self) trả về len(self.log) > 0.

⑤ Tạo error_log = Logger("error"), rồi chạy theo thứ tự: print(bool(error_log))error_log("Lỗi DB")print(len(error_log))print(bool(error_log)).

Python Editor

Chạy code để xem đầu ra

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 methodCú phápKhi nào được gọi
__init__Money(300)Khi tạo instance
__add__a + bToán tử +
__str__print(p) / str(p)Chuỗi hướng người dùng
__repr__repr(p) / hiển thị REPLChuỗi hướng dev
__eq__a == bSo sánh đẳng thức
__call__obj(...)Gọi kiểu hàm
__len__len(obj)Độ dài
__bool__if obj: / bool(obj)Truthiness
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Khi result = a + b chạy, Python gọi method nào ngầm bên dưới?

Câu 2Mô tả nào về __str____repr__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.)