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

Đ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 từ ba lớp cha
Animalname / speakSwimmerswimFlyerflyDuckspeak (override)kế thừakế thừakế thừa
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.

Xây dựng Duck gói ba lớp cha.

① Định nghĩa class Animal: với __init__(self, name) để gán self.name = name.

② Trong class Swimmer:, định nghĩa swim(self) trả về f"{self.name} đang bơi". Làm tương tự cho Flyer với fly(self).

③ Định nghĩa class Duck(Animal, Swimmer, Flyer): với speak(self) trả về f"{self.name} kêu quack".

④ Tạo duck = Duck("Donald")print giá trị trả về của speak(), swim(), và fly().

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

Python Editor

Chạy code để xem đầu ra

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ả SwimmerFlyer đề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
MRO duyệt (A, B, C) từ trái sang phải
Duck().move()move trênDuck?không→ tiếp→ chạymove trênSwimmer?→ chạy
Với 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.

Xác nhận bằng thử nghiệm nhanh rằng thứ tự bạn liệt kê lớp cha thay đổi kết quả.

① Trong class Swimmer:, định nghĩa move(self) trả về "đang bơi". Làm tương tự cho Flyer với "đang bay".

② Định nghĩa class Duck(Swimmer, Flyer):class Goose(Flyer, Swimmer):, cả hai chỉ với pass.

print kết quả của Duck().move()Goose().move() — xem đảo thứ tự lớp cha đảo kết quả thế nào.

Python Editor

Chạy code để xem đầu ra

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: BC mỗi cái kế thừa từ A, và D kế thừa từ cả BC — vẽ đồ thị kế thừa cho ra hình kim cương theo nghĩa đen.

Hình dạng kế thừa kim cương
A(cha chung)Bkế thừa ACkế thừa ADkế thừa B và Ckế thừakế thừakế thừakế thừa
A là tổ tiên chung. BC mỗi cái kế thừa từ A. D rồi kế thừa từ cả BC — 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() vs gọi bằng tên class
C().hello()super().hello()thứ tự MRO(chỉ một)A.hello(self)B.hello(self)cả A và Bgọi được
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

Viết class C gọi hello của cả hai lớp cha bằng tên.

① Định nghĩa class A: với hello(self) chạy print("hello từ A").

② Định nghĩa class B: với hello(self) chạy print("hello từ B").

③ Định nghĩa class C(A, B): với hello(self) gọi A.hello(self)B.hello(self)print("hello từ C") theo thứ tự đó (đừng quên truyền self).

④ Chạy C().hello() và xác nhận in ra ba dòng.

Python Editor

Chạy code để xem đầu ra

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 chaclass Child(A, B): ...
Xem lớp cha nào thắngTênClass.__mro__ (CPython)
Chỉ gọi cái kế tiếp trong MROsuper().method(...)
Nhắm một lớp cha cụ thểLớpCha.method(self, ...)
QUIZ

Kiểm tra kiến thức

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

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?

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à?