2.22. Kế thừa¶
Kế thừa cho phép một lớp mở rộng một lớp khác -- bắt đầu với tất cả các phương thức và thuộc tính của nó, sau đó thêm hoặc ghi đè những gì khác biệt. Lớp gốc được gọi là lớp cơ sở (hoặc lớp cha); phần mở rộng được gọi là lớp con.
Square tái sử dụng mọi phương thức của Shape và ghi đè những phương thức cần chuyên biệt hóa.¶
2.22.1. Định nghĩa lớp con¶
Lớp cơ sở được đặt trong dấu ngoặc đơn trên dòng class:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
Đầu ra:
square with area 16
Square kế thừa describe từ Shape nguyên vẹn và ghi đè area để trả về giá trị thực. Phương thức describe của lớp cơ sở vẫn hoạt động vì nó gọi self.area() -- được giải quyết thành phiên bản ghi đè lúc chạy.
2.22.2. super()¶
super() trả về một proxy cho phép một phương thức gọi phiên bản lớp cơ sở của một phương thức. Cách dùng phổ biến nhất là gọi __init__ của lớp cơ sở từ __init__ của lớp con để lớp cơ sở có thể khởi tạo các thuộc tính của riêng nó:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
Quên super().__init__ là nguyên nhân phổ biến của các lỗi: các thuộc tính mà lớp cơ sở lẽ ra đã thiết lập không bao giờ được thiết lập, và các phương thức kế thừa phụ thuộc vào chúng sẽ gặp sự cố sau này.
2.22.2.1. Lớp cơ sở ngầm định¶
Mọi lớp đều ngầm kế thừa từ object, gốc của hệ thống phân cấp kiểu của Python. Các phương thức như __repr__, __eq__, và cơ chế đằng sau truy cập thuộc tính đều đến từ đó ngay cả khi một lớp không khai báo lớp cơ sở nào. Viết class Foo(object): tường minh và viết class Foo: là tương đương trong Python hiện đại; dạng sau là dạng quy ước.
2.22.3. Khi nào kế thừa hữu ích -- và khi nào thì không¶
Hãy dùng kế thừa khi một lớp thực sự là loại cụ thể hơn của lớp khác, chia sẻ hầu hết hành vi. Bài kiểm tra cổ điển là kiểm tra "is-a": một Square là một Shape.
Khi hai lớp chỉ tình cờ chia sẻ một vài hàm trợ giúp, kế thừa là quá mức. Hãy dùng kết hợp thay thế: để một lớp giữ một thể hiện của lớp kia như một thuộc tính và sử dụng nó qua thuộc tính đó. Hình thức là "has-a", không phải "is-a":
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
Cả hai đều hoạt động. Phiên bản kết hợp thường tốt hơn vì nó:
Giữ giao diện nhỏ gọn.
WorkerComposeschỉ exposerunvàlogger.WorkerInheritscũng exposelog-- người gọi có thể viếtworker.log(...)trực tiếp, dù điều đó có được dự định hay không.Tách biệt vòng đời. Logger có thể được hoán đổi, chia sẻ giữa các worker, hoặc được xây dựng khác nhau cho mỗi worker, tất cả mà không cần chạm vào lớp
Worker. Một lớp con được gắn chặt với lớp cơ sở.Tránh va chạm tên phương thức. Hai lớp cơ sở cùng định nghĩa
runsẽ xung đột khi một lớp cố kế thừa từ cả hai; hai thuộc tính không bao giờ xung đột.
Quy tắc thực tế:
Hãy dùng kế thừa khi lớp con thực sự là một loại của lớp cơ sở và mọi phương thức trên lớp cơ sở cũng có ý nghĩa với nó --
Squarelà mộtShape;MemoryErrorlà mộtException.Hãy dùng kết hợp cho mọi thứ còn lại -- khi bạn chỉ muốn hành vi của một lớp khác, không phải danh tính của nó. Hầu hết code thực tế sử dụng kết hợp nhiều hơn kế thừa rất nhiều.