2.22. الوراثة¶
تتيح الوراثة لصنف أن يوسّع صنفاً آخر -- يبدأ بكل توابعه وسماته، ثم يضيف أو يتجاوز ما هو مختلف. ويُسمى الأصل القاعدة (أو الأب)؛ ويُسمى التوسيع الصنف الفرعي.
يعيد 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.استخدم التركيب لكل ما عدا ذلك -- عندما تريد فقط سلوك صنف آخر، لا هويته. ومعظم الشيفرات الحقيقية تستخدم التركيب أكثر بكثير مما تستخدم الوراثة.