Một lớp được xây dựng trên lớp khác
Kế thừa (inheritance) cho phép bạn định nghĩa một lớp mới như một bản mở rộng của lớp có sẵn. Lớp có sẵn là lớp cha (còn gọi là lớp cơ sở hay superclass), còn lớp mới là lớp con (subclass). Lớp con nhận mọi phương thức và thuộc tính lớp của lớp cha mà không phải viết lại, và có thể thêm cái riêng hoặc thay thế những cái cần hoạt động khác đi.
Bạn khai báo lớp cha trong ngoặc tròn sau tên lớp:
Đầu ra:
Rex is an animal
Woof
Dog không có __init__ và cũng không có describe riêng. Khi bạn gọi Dog("Rex"), Python tìm __init__ trên Dog, không thấy, nên dùng cái trên Animal. Việc tìm kiếm tương tự diễn ra với rex.describe(). Với rex.speak(), Python tìm thấy speak trên Dog trước và dừng ở đó.
Thứ tự tìm kiếm đó chính là toàn bộ cơ chế: Python tìm trong lớp của chính đối tượng, rồi đến lớp cha, rồi lớp cha của lớp cha, và dùng kết quả khớp đầu tiên. Nếu bạn mới làm quen với lớp, trang về lớp giới thiệu __init__, self và phương thức trước.
Ghi đè một phương thức
Một phương thức trong lớp con trùng tên với phương thức của lớp cha sẽ thay thế nó đối với các instance của lớp con. Đây gọi là ghi đè (overriding). Mỗi lớp con có thể trả lời cùng một lời gọi theo cách riêng:
Đầu ra:
Rectangle with area 12
Square with area 25
Shape with area 0
summary chỉ được định nghĩa một lần, trên Shape, nhưng nó gọi self.area(). Vì self là đối tượng thực tế, một Rectangle sẽ chạy Rectangle.area, còn một Square tìm thấy area trên lớp cha Rectangle của nó. Code của lớp cha gọi phương thức của lớp con mà không cần biết lớp con tồn tại. Đây là điều khiến kế thừa hữu ích để chia sẻ một quy trình chung trong khi để mỗi lớp con tự điền các chi tiết.
Gọi lớp cha với super()
Khi lớp con định nghĩa __init__ riêng, __init__ của lớp cha không còn tự chạy nữa. Nếu lớp con vẫn cần phần thiết lập của lớp cha, hãy gọi nó bằng super().__init__():
Đầu ra:
Ada 1050.0
super() trả về một đối tượng tìm kiếm phương thức bắt đầu từ lớp cha của lớp hiện tại. Vì vậy super().__init__(owner, balance) chạy Account.__init__ trên cùng đối tượng đó, và sau đó self.owner cùng self.balance đều tồn tại.
Bỏ dòng đó đi thì đối tượng chỉ được dựng một nửa:
Đầu ra:
0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'
Bạn có thể viết Account.__init__(self, owner) thay thế, và code cũ thường làm vậy. super() được ưu tiên hơn vì nó không lặp lại tên lớp cha và phối hợp đúng với đa kế thừa, sẽ được nói ở phần dưới.
Mở rộng phương thức thay vì thay thế
super() dùng được trong bất kỳ phương thức nào, không chỉ __init__. Gọi phiên bản của lớp cha bên trong phương thức ghi đè giúp bạn bổ sung hành vi xung quanh nó thay vì viết lại từ đầu:
Đầu ra:
started
[12:00] started
INFO [12:00] started
Mỗi cấp thêm một phần và chuyển phần còn lại lên trên. Nếu sau này format của lớp cha thay đổi, cả hai lớp con đều nhận được thay đổi đó.
isinstance và issubclass
Một instance của lớp con cũng là instance của mọi lớp phía trên nó. isinstance(obj, cls) kiểm tra quan hệ đó, còn issubclass(child, parent) kiểm tra quan hệ giữa hai lớp:
Hãy ưu tiên isinstance thay vì so sánh trực tiếp type(obj). Một phép kiểm tra như type(obj) is Animal sẽ loại bỏ mọi lớp con, và hỏng ngay khi có người thêm Dog.
Mọi lớp bạn viết đều kế thừa từ object, kể cả khi bạn không ghi ra. class Animal: giống hệt class Animal(object):. Đó là nơi các phương thức mặc định như __str__, __repr__ và __eq__ đến từ, và là lý do việc ghi đè __str__ làm thay đổi những gì print() hiển thị:
Đầu ra:
(2, 3)
True
Bắt buộc lớp con cài đặt một phương thức
Đôi khi lớp cha không bao giờ nên được dùng trực tiếp, mà chỉ làm khuôn mẫu. Module abc đánh dấu một lớp như vậy là lớp trừu tượng. Mọi phương thức có decorator @abstractmethod phải được ghi đè thì lớp mới tạo được instance:
Ba dòng đầu tiên của đầu ra là kết quả xuất:
exporting 2 rows
1,2
3,4
Dòng cuối cùng là một TypeError nêu tên Exporter và phương thức export còn thiếu của nó (câu chữ chính xác khác nhau giữa các phiên bản Python). Lỗi lộ ra ngay khi tạo đối tượng, chứ không phải đợi đến khi export cuối cùng được gọi.
Đa kế thừa và MRO
Một lớp có thể liệt kê nhiều hơn một lớp cha. Khi đó nó kế thừa từ tất cả:
Đầu ra:
paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']
Cả hai lớp cha đều định nghĩa move, và Duck nhận cái của Swimmer. Python quyết định dựa vào thứ tự phân giải phương thức (MRO): một danh sách các lớp duy nhất, lưu trong Duck.__mro__, mà mọi lần tìm thuộc tính đều duyệt từ đầu đến cuối. Danh sách bắt đầu bằng chính lớp đó, giữ các lớp cha theo thứ tự bạn đã liệt kê, đặt mỗi lớp trước các lớp cha của nó, và kết thúc bằng object. Đổi thứ tự thành class Duck(Flyer, Swimmer) thì move trả về "flies".
super() đi theo cùng danh sách đó. Bên trong một phương thức của Swimmer, super() nghĩa là "lớp kế tiếp sau Swimmer trong MRO của lớp đối tượng", mà với một Duck thì đó là Flyer, không phải object. Đó là cách các lớp phối hợp với nhau: mỗi lớp đều gọi super() và mọi lớp trong chuỗi chạy đúng một lần, kể cả trong hình kim cương, khi hai lớp cha có chung một lớp ông.
Trong thực tế, đa kế thừa chủ yếu được dùng cho mixin: những lớp nhỏ bổ sung một khả năng, như JsonMixin thêm phương thức to_json, kết hợp với một lớp cha chính. Các hệ phân cấp sâu với nhiều lớp cha đầy đủ rất khó theo dõi và hiếm khi cần đến.
Kế thừa hay composition?
Kế thừa mô tả quan hệ "là một". Một SavingsAccount là một Account: mọi code làm việc với Account đều phải chạy được với nó mà không cần sửa. Khi câu đó nghe không ổn, có lẽ bạn cần composition, mô tả quan hệ "có một": đối tượng giữ một đối tượng khác làm thuộc tính và gọi đến nó.
Đầu ra:
Roadster: 300 hp engine running
class Car(Engine) cũng chạy được, nhưng như vậy là khẳng định xe hơi là một loại động cơ, cho Car mọi phương thức của động cơ, và khiến bạn không thể thay một động cơ khác mà không tạo lớp khác. Với composition, các bộ phận vẫn tách biệt, và thay động cơ chỉ là chuyện truyền vào một đối tượng khác.
Vài dấu hiệu cho thấy kế thừa là công cụ sai:
- Lớp con ghi đè hầu hết phương thức của lớp cha, hoặc vô hiệu hóa một số phương thức.
- Bạn kế thừa chỉ để dùng lại một hai phương thức trợ giúp.
- Hệ phân cấp vượt quá hai ba cấp và bạn phải đọc nhiều tệp mới biết một phương thức làm gì.
Những lỗi thường gặp
- Quên
super().__init__().__init__của lớp con thay thế__init__của lớp cha, nên các thuộc tính của lớp cha không bao giờ được đặt và về sau bạn gặpAttributeError. - Truyền
selfvào lời gọisuper(). Viếtsuper().__init__(name), đừng viếtsuper().__init__(self, name).super()đã gắn phương thức với đối tượng hiện tại rồi. - Chữ ký không khớp khi ghi đè. Nếu
speak(self)của lớp cha được code dùng chung gọi đến, mộtspeak(self, volume)ở lớp con với đối số bắt buộc thêm vào sẽ làm hỏng code đó. Giữ phương thức ghi đè tương thích, hoặc cho tham số mới giá trị mặc định. - Kiểm tra
type(obj) == SomeClass. Cách này bỏ qua lớp con. Hãy dùngisinstance. - Dùng kế thừa để chia sẻ code giữa các lớp không liên quan. Hãy chuyển phần code dùng chung vào một hàm hoặc một đối tượng trợ giúp.
Câu hỏi thường gặp
Làm sao để kế thừa một lớp trong Python?
Đặt lớp cha trong ngoặc tròn sau tên lớp mới: class Dog(Animal):. Dog nhận mọi phương thức và thuộc tính lớp của Animal, và có thể thêm cái mới hoặc ghi đè những cái đã có.
super().__init__() dùng để làm gì?
Nó gọi __init__ của lớp cha để lớp cha thiết lập các thuộc tính của chính nó. Khi lớp con định nghĩa __init__ riêng, Python không tự động gọi __init__ của lớp cha, nên bạn gọi super().__init__(...) với các đối số mà lớp cha cần, rồi mới đặt các thuộc tính bổ sung của lớp con.
isinstance và type trong Python khác nhau thế nào?
isinstance(obj, Animal) là True với một Animal và với instance của bất kỳ lớp con nào của Animal. type(obj) is Animal chỉ True khi đối tượng được tạo trực tiếp từ chính Animal. Hãy ưu tiên isinstance, vì nó vẫn đúng khi có người thêm một lớp con.
Python có hỗ trợ đa kế thừa không?
Có: class C(A, B): kế thừa từ cả hai. Khi cả hai lớp cha cùng định nghĩa một phương thức, Python chọn một cái theo thứ tự phân giải phương thức (MRO), mà bạn có thể xem bằng C.__mro__. Danh sách này liệt kê C đầu tiên, rồi đến các lớp cha từ trái sang phải, và object ở cuối.
Khi nào nên dùng composition thay vì kế thừa?
Dùng kế thừa cho quan hệ "là một", khi lớp con có thể dùng ở mọi nơi lớp cha được dùng (một SavingsAccount là một Account). Dùng composition, tức lưu một đối tượng làm thuộc tính, cho quan hệ "có một" (một Car có một Engine). Khi còn phân vân, composition dễ thay đổi về sau hơn.