Câu 1Trong class Duck(Swimmer, Flyer):, nếu cả hai lớp cha định nghĩa method move, cái nào thắng?
Đa kế thừa và MRO — Kế thừa từ nhiều lớp cha
Học đa kế thừa trong Python. Kết hợp các lớp cha với class Child(A, B):, xem method resolution order (MRO) chọn giữa các method cùng tên thế nào, và kiểm tra thứ tự với __mro__.
Lần trước ta đã bao quát kế thừa đơn, override, và super(). Python cho phép bạn kế thừa từ nhiều lớp cha cùng lúc — đó là đa kế thừa. Lần này ta bao quát cú pháp và quy tắc quyết định lớp cha nào được gọi khi nhiều cha có cùng tên method: method resolution order (MRO).
Cú pháp cơ bản
Cú pháp đơn giản — liệt kê các lớp cha cách nhau bằng dấu phẩy. Viết class Duck(Animal, Swimmer, Flyer): nghĩa là Duck kế thừa thuộc tính và method từ tất cả Animal, Swimmer, và Flyer.
Trong ví dụ dưới, Animal mang tên và hành vi nói, Swimmer thêm bơi, Flyer thêm bay — và Duck gói cả ba thành một sinh vật đa năng.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Swimmer:
def swim(self):
return f"{self.name} đang bơi"
class Flyer:
def fly(self):
return f"{self.name} đang bay"
class Duck(Animal, Swimmer, Flyer): # đa kế thừa
def speak(self):
return f"{self.name} kêu quack"
duck = Duck("Donald")
print(duck.speak()) # Donald kêu quack
print(duck.swim()) # Donald đang bơi
print(duck.fly()) # Donald đang bay
Duck kế thừa thuộc tính và __init__ từ Animal, swim từ Swimmer, và fly từ Flyer. Chỉ speak được chính Duck override.Method resolution order (MRO) — Lớp cha nào thắng?
Sự việc trở nên rắc rối khi nhiều lớp cha có method cùng tên. Nếu cả Swimmer và Flyer đều định nghĩa method move, cái nào chạy khi bạn gọi move trên một instance Duck?
Python dùng method resolution order (MRO) — một thứ tự tìm cố định — để duyệt các class từ trên xuống và chạy match đầu tiên. Với đa kế thừa, thứ tự là trái sang phải như viết trong class Duck(A, B, C):. Nên method cùng tên của A thắng; nếu không có, thử B, rồi C.
class Swimmer:
def move(self):
return "đang bơi"
class Flyer:
def move(self):
return "đang bay"
class Duck(Swimmer, Flyer): # trái thắng = Swimmer.move
pass
class Goose(Flyer, Swimmer): # đảo thứ tự = kết quả khác
pass
print(Duck().move()) # đang bơi
print(Goose().move()) # đang bay
Duck(Swimmer, Flyer), Python xem Swimmer trước, rồi Flyer. Nếu cả hai đều định nghĩa move, Swimmer.move thắng. Đổi thứ tự lớp cha thì kết quả khác.Trên CPython bạn có thể đọc thứ tự thực tế với TênClass.__mro__ (runtime ở đây là MicroPython, không có thuộc tính __mro__, nên đoạn code dưới chỉ là tham khảo cho những gì bạn sẽ thấy trên Python tiêu chuẩn).
# Những gì CPython sẽ hiển thị
class Swimmer:
pass
class Flyer:
pass
class Duck(Swimmer, Flyer):
pass
for cls in Duck.__mro__:
print(cls.__name__)
# Duck
# Swimmer
# Flyer
# object
object ở cuối là gì?
Mọi class trong Python cuối cùng đều kế thừa từ class có sẵn object. Ngay cả khi bạn không viết, class Foo: về bản chất là class Foo(object):. Đó là lý do __mro__ luôn kết thúc bằng object.
Kế thừa kim cương và linearization C3
Một dạng nữa hay xuất hiện là kế thừa kim cương: B và C mỗi cái kế thừa từ A, và D kế thừa từ cả B và C — vẽ đồ thị kế thừa cho ra hình kim cương theo nghĩa đen.
A là tổ tiên chung. B và C mỗi cái kế thừa từ A. D rồi kế thừa từ cả B và C — các mũi tên tạo thành kim cương.Python tính MRO bằng thuật toán linearization C3, tạo ra thứ tự thông minh «thử các con cháu (B, C) trước, rồi đi lên tổ tiên chung (A) cuối cùng».
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C): # kim cương
pass
print(D().method()) # B
Trong ví dụ này D().method() trả "B" vì MRO là D → B → C → A. Nếu B không định nghĩa method, method của C sẽ chạy; không có cái đó, method của A. «Trái trước, nhưng tổ tiên chung cuối cùng» — đó là cheat sheet cho kế thừa kim cương.
Gọi method của một lớp cha cụ thể bằng tên class
Khi bạn cụ thể muốn gọi method của một lớp cha cụ thể — không phải chỉ cái thắng MRO — super() chỉ cho bạn class kế tiếp trong MRO, nên bạn chỉ gọi được tối đa một phiên bản của lớp cha. Thay vào đó, dùng LớpCha.method(self, ...) để chọn chính xác cái bạn muốn.
Vì cuộc gọi đi qua phía class, bạn phải truyền self tự mình làm đối số đầu — đặc điểm duy nhất cần nhớ.
super() chạy chỉ một method (cái kế tiếp trong MRO). Gọi bằng tên class như A.hello(self) cho bạn nhắm tới một lớp cha cụ thể bất kể MRO — tiện khi bạn muốn gọi nhiều method cha tuần tự.class A:
def hello(self):
print("hello từ A")
class B:
def hello(self):
print("hello từ B")
class C(A, B):
def hello(self):
A.hello(self) # gọi tường minh hello của A
B.hello(self) # gọi tường minh hello của B
print("hello từ C")
C().hello()
# hello từ A
# hello từ B
# hello từ C
Mạnh, nhưng dùng tiết chế
Đa kế thừa tiện nhưng bạn phải giữ MRO trong đầu liên tục để theo dõi hành vi — đó là chi phí nhận thức thực sự. Trong thực tế, thay vì ép đa kế thừa, dùng một lớp cha và giữ các class tính năng nhỏ làm thuộc tính (composition) thường rõ hơn. Để dành đa kế thừa cho các trường hợp như trộn các năng lực độc lập kiểu Swimmer / Flyer.
| Bạn muốn làm gì | Cách viết |
|---|---|
| Kế thừa nhiều lớp cha | class Child(A, B): ... |
| Xem lớp cha nào thắng | TênClass.__mro__ (CPython) |
| Chỉ gọi cái kế tiếp trong MRO | super().method(...) |
| Nhắm một lớp cha cụ thể | LớpCha.method(self, ...) |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Cách đúng để đọc method resolution order (MRO) của một class? (CPython)
Câu 3Trong đa kế thừa, cách đúng để gọi method của một lớp cha cụ thể bằng tên là?