Menu
flag Ar iconالعربيةdown icon

الوراثة في بايثون: الكلاسات الفرعية وsuper() وإعادة التعريف وMRO

كيف تعمل وراثة الكلاسات في بايثون: إنشاء كلاس فرعي، واستدعاء super().__init__، وإعادة تعريف التوابع وتوسيعها، وisinstance وissubclass، والوراثة المتعددة وMRO، ومتى يكون التركيب هو الخيار الأفضل.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

كلاس مبني على كلاس آخر

تتيح لك الوراثة تعريف كلاس جديد على أنه امتداد لكلاس موجود. الكلاس الموجود هو الأب (ويُسمى أيضًا الكلاس الأساسي أو superclass)، والجديد هو الابن (الكلاس الفرعي أو subclass). يحصل الابن على كل توابع الأب وخصائص الكلاس فيه دون أن تكررها، ويمكنه أن يضيف توابعه الخاصة أو يستبدل تلك التي يجب أن تتصرف بشكل مختلف.

تكتب الكلاس الأب بين قوسين بعد اسم الكلاس:

المخرجات:

Rex is an animal
Woof

لا يملك Dog تابع __init__ ولا تابع describe خاصًا به. عندما تستدعي Dog("Rex")، تبحث بايثون عن __init__ في Dog، فلا تجده، فتستخدم التابع الموجود في Animal. ويحدث البحث نفسه مع rex.describe(). أما مع rex.speak() فتجد speak في Dog أولًا وتتوقف عنده.

ترتيب البحث هذا هو الآلية كلها: تبحث بايثون في كلاس الكائن نفسه، ثم في الأب، ثم في أب الأب، وتستخدم أول تطابق. إذا كانت الكلاسات جديدة عليك، فابدأ بـصفحة الكلاسات التي تشرح __init__ وself والتوابع.

إعادة تعريف تابع

التابع الموجود في الابن بالاسم نفسه الموجود في الأب يحل محله في نسخ الابن. يُسمى هذا إعادة التعريف (overriding). ويستطيع كل كلاس فرعي أن يجيب عن الاستدعاء نفسه بطريقته الخاصة:

المخرجات:

Rectangle with area 12
Square with area 25
Shape with area 0

يُعرَّف summary مرة واحدة في Shape، لكنه يستدعي self.area(). ولأن self هو الكائن الفعلي، فإن Rectangle ينفّذ Rectangle.area، ويجد Square التابع area في أبيه Rectangle. كود الأب يستدعي تابع الابن دون أن يعرف بوجود الابن. وهذا ما يجعل الوراثة مفيدة لمشاركة سير عمل مشترك مع ترك التفاصيل لكل كلاس فرعي.

استدعاء الأب باستخدام super()

عندما يعرّف الابن __init__ خاصًا به، لا يعود __init__ الخاص بالأب يُنفَّذ من تلقاء نفسه. فإذا كان الابن لا يزال يحتاج إلى تهيئة الأب، فاستدعها باستخدام super().__init__():

المخرجات:

Ada 1050.0

تُرجع super() كائنًا يبحث عن التوابع بدءًا من أب الكلاس الحالي. لذلك تنفّذ super().__init__(owner, balance) التابع Account.__init__ على الكائن نفسه، وتصبح self.owner وself.balance موجودتين بعد ذلك.

إذا حذفت ذلك السطر، يبقى الكائن مبنيًا نصف بناء:

المخرجات:

0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'

يمكنك أن تكتب Account.__init__(self, owner) بدلًا من ذلك، والكود القديم يفعل هذا كثيرًا. لكن super() مفضّلة لأنها لا تكرر اسم الأب، وتتعاون بشكل صحيح مع الوراثة المتعددة التي نشرحها لاحقًا.

توسيع تابع بدلًا من استبداله

تعمل super() في أي تابع، وليس في __init__ وحده. استدعاء نسخة الأب داخل التابع المُعاد تعريفه يتيح لك إضافة سلوك حولها بدلًا من إعادة كتابتها:

المخرجات:

started
[12:00] started
INFO [12:00] started

كل مستوى يضيف قطعة واحدة ويفوّض الباقي إلى الأعلى. وإذا تغيّر format في الأب لاحقًا، يلتقط الابنان التغيير كلاهما.

isinstance وissubclass

نسخة الكلاس الفرعي هي أيضًا نسخة من كل كلاس فوقه. تفحص isinstance(obj, cls) هذه العلاقة، وتفحصها issubclass(child, parent) بين كلاسين:

فضّل isinstance على مقارنة type(obj) مباشرة. فحص مثل type(obj) is Animal يرفض كل كلاس فرعي، وينكسر بمجرد أن يضيف أحدهم Dog.

كل كلاس تكتبه يرث من object، حتى لو لم تذكر ذلك. class Animal: هي نفسها class Animal(object):. ومن هناك تأتي التوابع الافتراضية مثل __str__ و__repr__ و__eq__، ولهذا تغيّر إعادة تعريف __str__ ما تعرضه print():

المخرجات:

(2, 3)
True

إلزام الكلاسات الفرعية بتنفيذ تابع

أحيانًا لا ينبغي استخدام الأب مباشرة أبدًا، بل كقالب فقط. تُعلِّم الوحدة abc مثل هذا الكلاس على أنه مجرد (abstract). وأي تابع مزيّن بـ@abstractmethod يجب إعادة تعريفه قبل أن يمكن إنشاء نسخة من الكلاس:

الأسطر الثلاثة الأولى من المخرجات هي نتيجة التصدير:

exporting 2 rows
1,2
3,4

السطر الأخير خطأ TypeError يذكر Exporter والتابع export الناقص فيه (تختلف الصياغة الدقيقة بين إصدارات بايثون). يظهر الخطأ عند إنشاء الكائن، وليس لاحقًا عندما يُستدعى export أخيرًا.

الوراثة المتعددة وMRO

يمكن للكلاس أن يذكر أكثر من أب واحد، فيرث منهم جميعًا:

المخرجات:

paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']

كلا الأبوين يعرّف move، ويحصل Duck على التابع الموجود في Swimmer. تحسم بايثون ذلك بترتيب تحليل التوابع (MRO): قائمة واحدة من الكلاسات مخزّنة في Duck.__mro__، يمر عليها كل بحث عن خاصية من أولها إلى آخرها. تبدأ القائمة بالكلاس نفسه، وتحافظ على الآباء بالترتيب الذي كتبتهم به، وتضع كل كلاس قبل آبائه، وتنتهي بـobject. بدّل الترتيب إلى class Duck(Flyer, Swimmer) فيُرجع move القيمة "flies".

وتتبع super() القائمة نفسها. داخل تابع في Swimmer، تعني super() "الكلاس التالي بعد Swimmer في MRO الخاص بكلاس الكائن"، وهو Flyer في حالة Duck، وليس object. وبهذا تستطيع الكلاسات المتعاونة أن تستدعي كل منها super() فيُنفَّذ كل كلاس في السلسلة مرة واحدة بالضبط، حتى في شكل الماسة (diamond) حيث يشترك أبوان في جد واحد.

عمليًا، تُستخدم الوراثة المتعددة غالبًا مع الـmixins: كلاسات صغيرة تضيف قدرة واحدة، مثل JsonMixin الذي يضيف تابع to_json، وتُدمج مع أب رئيسي واحد. أما التسلسلات الهرمية العميقة ذات الآباء الكاملين المتعددين فيصعب تتبعها ونادرًا ما تحتاجها.

الوراثة أم التركيب؟

تمثّل الوراثة علاقة "هو نوع من". SavingsAccount هو Account: أي كود يعمل مع Account يجب أن يعمل معه دون تغيير. وعندما تبدو هذه الجملة خاطئة، فأنت على الأرجح تحتاج إلى التركيب (composition)، الذي يمثّل علاقة "يملك": يحتفظ الكائن بكائن آخر كخاصية ويستدعيه.

المخرجات:

Roadster: 300 hp engine running

class Car(Engine) سيعمل أيضًا، لكنه سيدّعي أن السيارة نوع من المحركات، ويعطي Car كل توابع المحرك، ويجعل تركيب محرك مختلف مستحيلًا دون كلاس مختلف. أما مع التركيب فتبقى الأجزاء منفصلة، واستبدال المحرك مسألة تمرير كائن آخر.

بعض العلامات على أن الوراثة هي الأداة الخطأ:

  • الابن يعيد تعريف معظم توابع الأب، أو يعطّل بعضها.
  • ترث فقط لتعيد استخدام تابع مساعد أو اثنين.
  • يتجاوز التسلسل الهرمي مستويين أو ثلاثة، وتضطر إلى قراءة عدة ملفات لتعرف ما يفعله تابع ما.

أخطاء شائعة

  • نسيان super().__init__(). __init__ في الابن يحل محل __init__ في الأب، فلا تُضبط خصائص الأب أبدًا، وتحصل على AttributeError لاحقًا.
  • تمرير self في استدعاءات super(). اكتب super().__init__(name)، وليس super().__init__(self, name). فـsuper() تربط التابع بالكائن الحالي مسبقًا.
  • توقيعات غير متطابقة عند إعادة التعريف. إذا كان كود مشترك يستدعي speak(self) الموجود في الأب، فإن speak(self, volume) في الابن مع وسيط إضافي إلزامي يكسر ذلك الكود. اجعل إعادة التعريف متوافقة، أو أعطِ المعاملات الجديدة قيمًا افتراضية.
  • فحص type(obj) == SomeClass. يتجاهل الكلاسات الفرعية. استخدم isinstance.
  • استخدام الوراثة لمشاركة الكود بين كلاسات لا علاقة بينها. انقل الكود المشترك إلى دالة أو كائن مساعد بدلًا من ذلك.

الأسئلة الشائعة

كيف ترث من كلاس في بايثون؟

ضع الكلاس الأب بين قوسين بعد اسم الكلاس الجديد: class Dog(Animal):. يحصل Dog على كل توابع Animal وخصائص الكلاس فيه، ويمكنه أن يضيف توابع جديدة أو يعيد تعريف الموجودة.

ماذا تفعل super().__init__()؟

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

ما الفرق بين isinstance وtype في بايثون؟

isinstance(obj, Animal) تساوي True لكائن من Animal ولنسخ أي كلاس فرعي من Animal. أما type(obj) is Animal فتساوي True فقط عندما يكون الكائن قد أُنشئ من Animal نفسه. فضّل isinstance، لأنها تبقى صحيحة عندما يضيف أحدهم كلاسًا فرعيًا.

هل تدعم بايثون الوراثة المتعددة؟

نعم: class C(A, B): يرث من الاثنين. وعندما يعرّف الأبوان التابع نفسه، تختار بايثون أحدهما باتباع ترتيب تحليل التوابع (MRO)، ويمكنك فحصه عبر C.__mro__. يأتي فيه C أولًا، ثم أبواه من اليسار إلى اليمين، وobject في النهاية.

متى أستخدم التركيب بدلًا من الوراثة؟

استخدم الوراثة لعلاقة "هو نوع من"، حيث يمكن استخدام الكلاس الفرعي في أي مكان يُتوقع فيه الأب (SavingsAccount هو Account). واستخدم التركيب، أي تخزين كائن كخاصية، لعلاقة "يملك" (Car يملك Engine). وإذا لم تكن متأكدًا، فالتركيب أسهل في التغيير لاحقًا.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن