2.22. การสืบทอด¶
การสืบทอดช่วยให้คลาส ขยาย คลาสอื่น -- เริ่มต้นด้วยเมธอดและ attribute ทั้งหมดของมัน แล้วเพิ่มหรือแทนที่สิ่งที่แตกต่าง ต้นฉบับเรียกว่า base (หรือ parent) ส่วนที่ขยายเรียกว่า subclass
Square ใช้เมธอดทุกตัวของ Shape ซ้ำและแทนที่เฉพาะที่จำเป็นต้องเฉพาะเจาะจง¶
2.22.1. การกำหนด subclass¶
คลาส base อยู่ในวงเล็บของบรรทัด 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())
ผลลัพธ์:
square with area 16
Square สืบทอด describe จาก Shape โดยไม่เปลี่ยนแปลงและแทนที่ area เพื่อคืนค่าจริง describe ของ base ยังทำงานได้เพราะมันเรียก self.area() -- ซึ่งแก้ไขเป็น override ในขณะรัน
2.22.2. super()¶
super() คืนค่า proxy ที่ให้เมธอดเรียกเวอร์ชัน base class ของเมธอดได้ การใช้งานที่พบบ่อยที่สุดคือการเรียก __init__ ของ base จาก __init__ ของ subclass เพื่อให้ base ได้เริ่มต้น attribute ของตัวเอง:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
การลืม super().__init__ เป็นแหล่งที่มาของบัคทั่วไป: attribute ที่ base จะตั้งค่าไม่ได้ถูกตั้งค่าเลย และเมธอดที่สืบทอดซึ่งพึ่งพา attribute เหล่านั้นจะ crash ในภายหลัง
2.22.2.1. คลาส base โดยนัย¶
คลาสทุกคลาสสืบทอดจาก object โดยนัย ซึ่งเป็นรากของลำดับชั้นประเภทของ Python เมธอดอย่าง __repr__, __eq__, และกลไกเบื้องหลังการเข้าถึง attribute ล้วนมาจากที่นั่นแม้เมื่อคลาสไม่ได้ประกาศ base การเขียน class Foo(object): อย่างชัดแจ้งและการเขียน class Foo: มีความเท่าเทียมกันใน Python สมัยใหม่ รูปแบบหลังเป็นรูปแบบตามธรรมเนียม
2.22.3. เมื่อการสืบทอดช่วยได้ -- และเมื่อไม่ช่วย¶
ใช้การสืบทอดเมื่อคลาสหนึ่งเป็น ประเภทที่เฉพาะเจาะจงกว่า ของอีกคลาสอย่างแท้จริง โดยแบ่งปันพฤติกรรมส่วนใหญ่ การทดสอบแบบ classic คือการตรวจสอบ "is-a": Square is a Shape
เมื่อสองคลาสเพียงแค่บังเอิญแบ่งปัน helper บางตัว การสืบทอดมากเกินไป ควรใช้ composition แทน: ให้คลาสหนึ่งถือ instance ของอีกคลาสเป็น attribute และใช้งานผ่าน attribute นั้น รูปแบบคือ "has-a" ไม่ใช่ "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")
ทั้งสองทำงานได้ เวอร์ชัน composition มักจะดีกว่าเพราะมัน:
ทำให้ interface เล็กลง
WorkerComposesเปิดเผยเพียงrunและloggerWorkerInheritsยังเปิดเผยlogด้วย -- ผู้เรียกสามารถเขียนworker.log(...)โดยตรงไม่ว่าจะตั้งใจหรือไม่แยกวงจรชีวิต logger สามารถสลับเปลี่ยน แบ่งปันระหว่าง worker หรือสร้างต่างกันต่อ worker ทั้งหมดโดยไม่ต้องแตะคลาส
Workersubclass ผูกติดกับ baseหลีกเลี่ยงการชนกันของชื่อเมธอด base สองตัวที่กำหนด
runทั้งคู่จะขัดแย้งกันเมื่อคลาสพยายามสืบทอดจากทั้งคู่ แต่ attribute สองตัวไม่มีทางขัดแย้ง
กฎเกณฑ์เชิงปฏิบัติ:
ใช้การสืบทอดเมื่อ subclass เป็น ประเภท ของ base อย่างแท้จริงและเมธอดทุกตัวของ base มีความหมายกับมันด้วย --
Squareis aShape;MemoryErroris anExceptionใช้ composition สำหรับทุกอย่างอื่น -- เมื่อคุณแค่ต้องการ พฤติกรรม ของคลาสอื่นไม่ใช่ตัวตนของมัน โค้ดจริงส่วนใหญ่ใช้ composition มากกว่าการสืบทอดอย่างมาก