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.

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