2.22. การสืบทอด

การสืบทอดช่วยให้คลาส ขยาย คลาสอื่น -- เริ่มต้นด้วยเมธอดและ attribute ทั้งหมดของมัน แล้วเพิ่มหรือแทนที่สิ่งที่แตกต่าง ต้นฉบับเรียกว่า base (หรือ parent) ส่วนที่ขยายเรียกว่า subclass

A base class Shape pointing down to a subclass Square; Square overrides one method (area) and inherits the rest.

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 และ logger WorkerInherits ยังเปิดเผย log ด้วย -- ผู้เรียกสามารถเขียน worker.log(...) โดยตรงไม่ว่าจะตั้งใจหรือไม่

  • แยกวงจรชีวิต logger สามารถสลับเปลี่ยน แบ่งปันระหว่าง worker หรือสร้างต่างกันต่อ worker ทั้งหมดโดยไม่ต้องแตะคลาส Worker subclass ผูกติดกับ base

  • หลีกเลี่ยงการชนกันของชื่อเมธอด base สองตัวที่กำหนด run ทั้งคู่จะขัดแย้งกันเมื่อคลาสพยายามสืบทอดจากทั้งคู่ แต่ attribute สองตัวไม่มีทางขัดแย้ง

กฎเกณฑ์เชิงปฏิบัติ:

  • ใช้การสืบทอดเมื่อ subclass เป็น ประเภท ของ base อย่างแท้จริงและเมธอดทุกตัวของ base มีความหมายกับมันด้วย -- Square is a Shape; MemoryError is an Exception

  • ใช้ composition สำหรับทุกอย่างอื่น -- เมื่อคุณแค่ต้องการ พฤติกรรม ของคลาสอื่นไม่ใช่ตัวตนของมัน โค้ดจริงส่วนใหญ่ใช้ composition มากกว่าการสืบทอดอย่างมาก