2.22. الوراثة

تتيح الوراثة لصنف أن يوسّع صنفاً آخر -- يبدأ بكل توابعه وسماته، ثم يضيف أو يتجاوز ما هو مختلف. ويُسمى الأصل القاعدة (أو الأب)؛ ويُسمى التوسيع الصنف الفرعي.

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

يعيد Square استخدام كل تابع في Shape ويتجاوز التوابع التي يحتاج إلى تخصيصها.

2.22.1. تعريف صنف فرعي

يوضع صنف القاعدة في الأقواس على سطر 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 الخاص بالقاعدة يعمل لأنه يستدعي self.area() -- الذي يُحلّ إلى التجاوز وقت التشغيل.

2.22.2. super()

تعيد super() وكيلاً يتيح لتابع أن يستدعي نسخة القاعدة من تابع. والاستخدام الأكثر شيوعاً هو استدعاء __init__ الخاص بالقاعدة من __init__ الخاص بالصنف الفرعي حتى تتمكن القاعدة من تهيئة سماتها الخاصة:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

إن نسيان super().__init__ مصدر شائع للعلل: فالسمات التي كانت القاعدة ستضبطها لا تُضبط أبداً، وتنهار لاحقاً التوابع الموروثة التي تعتمد عليها.

2.22.2.1. صنف القاعدة الضمني

يرث كل صنف ضمنياً من object، جذر تسلسل أنواع Python الهرمي. وتوابع مثل __repr__ و __eq__، والآلية الكامنة خلف الوصول إلى السمات، كلها تأتي من هناك حتى عندما لا يعلن صنفٌ أي قاعدة. وكتابة class Foo(object): صراحةً وكتابة class Foo: متكافئتان في Python الحديثة؛ والأخيرة هي الصيغة الاصطلاحية.

2.22.3. متى تساعد الوراثة -- ومتى لا تساعد

استخدم الوراثة عندما يكون أحد الأصناف حقاً نوعاً أكثر تحديداً من آخر، يتشارك معه معظم السلوك. والاختبار الكلاسيكي هو فحص "هو": Square هو Shape.

وعندما يتصادف أن يتشارك صنفان بضعة مساعدات فقط، تكون الوراثة مبالغة. الجأ إلى التركيب بدلاً من ذلك: اجعل صنفاً يحمل نسخة من الآخر كسمة ويستخدمها عبر تلك السمة. والصيغة هي "يملك"، لا "هو":

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")

كلاهما يعمل. والنسخة القائمة على التركيب أفضل عادةً لأنها:

  • تُبقي الواجهات صغيرة. يكشف WorkerComposes عن run و logger فقط. أما WorkerInherits فيكشف أيضاً عن log -- إذ يمكن للمستدعين كتابة worker.log(...) مباشرة، سواء قُصد ذلك أم لا.

  • تفصل أعمار الكائنات. يمكن استبدال logger، أو مشاركته بين العمال، أو بناؤه بشكل مختلف لكل عامل، كل ذلك دون المساس بصنف Worker. أما الصنف الفرعي فمثبَّت بقاعدته.

  • تتجنب تصادمات أسماء التوابع. قاعدتان تعرّفان كلتاهما run تتصادمان عندما يحاول صنف الوراثة من كلتيهما؛ أما السمتان فلا تتصادمان أبداً.

قاعدة عملية تقريبية:

  • استخدم الوراثة عندما يكون الصنف الفرعي حقاً نوعاً من القاعدة وكل تابع على القاعدة منطقي عليه أيضاً -- Square هو Shape؛ و MemoryError هو Exception.

  • استخدم التركيب لكل ما عدا ذلك -- عندما تريد فقط سلوك صنف آخر، لا هويته. ومعظم الشيفرات الحقيقية تستخدم التركيب أكثر بكثير مما تستخدم الوراثة.