الوراثة والتعدد الشكلي
درس 2 من 5
2الوراثة والتعدد الشكلي (Inheritance & Polymorphism)
الصورة الكبيرةكيف يرث صنف سلوك آخر ويعيد تعريف ما يحتاجه فقط، ولماذا سؤال «هل هو نوع منه؟» هو ما يقرر إن كانت الوراثة صحيحة أصلًا.
ماذا تشاهد في الرسم: Circle وSquare يرثان من Shape. الحلقة تنادي s.area() على كل شكل بالكلمة نفسها، فيجيب كل صنف بحسابه الخاص.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الوراثة مثل وصفة أم تأخذها ابنتها وتغيّر فيها التوابل فقط. لا تعيد كتابة الوصفة كلها، بل تقول «مثل وصفة أمي، لكن بفلفل أكثر». والتعدد الشكلي أن تقول لكل حيوان «تكلّم» فينبح الكلب ويموء القط: نفس الطلب، وكل واحد يجيب بطريقته.
class Dog(Animal): يرث السمات والتوابع، ويعيد تعريف ما يحتاجه override، ويستدعي الأصل بـsuper(). التعدد الشكلي أن يعمل الكود مع أي كائن يملك التابع المطلوب. والوراثة صحيحة فقط حين تكون العلاقة «هو نوع من» is-a، وإلا فالتركيب أنسب.
قبل أن تبدأ
🏛️درس الأصناف
تكتب صنفًا فيه __init__ وطريقة.
👨👧التشابه العائلي
الابن يشبه أباه في أشياء ويختلف في أخرى؛ هذه فكرة الوراثة.
الفكرة الجوهرية
الوراثة تعني أن صنفًا جديدًا يأخذ كل ما في صنف قديم ويغيّر ما يحتاج فقط. ثلاثة أمثلة تريك الأخذ، والتغيير، والإضافة.
1صنف يرث من صنف
تكتب اسم الأب بين قوسين بعد اسم الصنف الجديد. Cat لم تعرّف __init__، ومع ذلك أخذت الاسم، لأنها ورثته من Animal.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Cat(Animal):
def speak(self):
return "مياو"
pet = Cat("مشمش")
print(pet.name)
print(pet.speak())
لكنها عرّفت speak من جديد، فاستُخدمت نسختها هي لا نسخة الأب. هذا يسمّى «التجاوز».
2نفس النداء، سلوك مختلف
ثلاثة كائنات من ثلاثة أصناف، وحلقة واحدة تنادي speak على كلٍّ منها. كل كائن ردّ بطريقته دون أن تسأل الحلقة عن نوعه.
class Animal:
def speak(self):
return "..."
class Cat(Animal):
def speak(self):
return "مياو"
class Dog(Animal):
def speak(self):
return "هو"
for animal in [Cat(), Dog(), Animal()]:
print(animal.speak())
هذا هو «تعدّد الأشكال»، وفائدته أن الكود الذي يستخدم الكائنات لا يحتاج أن يعرف أصنافها بالتفصيل.
3أضف دون أن تعيد الكتابة
super().__init__(name) تنادي __init__ الأب لتحفظ الاسم كما كان، ثم تضيف أنت ما يخصّ الابن. وبهذا لا تكرّر كود الأب.
class Animal:
def __init__(self, name):
self.name = name
class Cat(Animal):
def __init__(self, name, indoor):
super().__init__(name)
self.indoor = indoor
c = Cat("لولو", True)
print(c.name, c.indoor)
print(isinstance(c, Animal), isinstance(c, Cat))
isinstance تؤكّد أن القطة حيوان وقطة في الوقت نفسه. الابن هو نوع من الأب دائمًا، والعكس غير صحيح.
__init__ جديد في الابن ونسيان نداء super().__init__. النتيجة أن سمات الأب لا تُنشأ أبدًا، وتأتيك رسالة AttributeError عند أول محاولة لقراءة self.name.كيف يعمل، خطوة بخطوة: كيف يجد بايثون الطريقة الصحيحة
تنادي s.area() على كائن ما.
بايثون يبحث عن area في صنف الكائن نفسه أولًا.
إن لم يجدها صعد إلى الأب، ثم إلى أريد الأب.
أول تعريف يجده هو الذي يُنفَّذ، ولهذا يغلب تعريف الابن.
أعمق قليلًا
الوراثة تتيح لصنف إعادة استخدام سلوك صنف آخر وتوسيعه. التعدد الشكلي (Polymorphism) يتيح لكائنات من أصناف مختلفة الاستجابة لنفس استدعاء الميثود كلٌ بطريقته الخاصة.
الرسم 6: Developer و Manager يرثان من Employee لكن كل واحد "يُعيد تعريف" (Override) calculate_bonus() بمنطقه الخاص.
class Employee:
def __init__(self, name, base_salary):
self.name = name
self.base_salary = base_salary
def calculate_bonus(self):
return self.base_salary * 0.05 # سلوك افتراضي عام
class Developer(Employee):
def calculate_bonus(self): # Override: يستبدل سلوك الأب
return self.base_salary * 0.10
class Manager(Employee):
def calculate_bonus(self):
return self.base_salary * 0.15
team = [Developer("خالد", 8000), Manager("منى", 12000)]
for member in team:
# نفس الاستدعاء calculate_bonus() لكن كل كائن يستجيب بطريقته -> تعدد شكلي
print(f"{member.name}: {member.calculate_bonus()}")
print(isinstance(team[0], Employee)) # True — Developer هو Employee أيضًا
print(issubclass(Manager, Employee)) # True — علاقة صنف بصنف
| الدالة | تتحقق من | مثال |
|---|---|---|
isinstance(obj, Cls) | هل هذا الكائن نسخة من هذا الصنف (أو أحد أبنائه)؟ | isinstance(dev, Employee) → True |
issubclass(A, B) | هل هذا الصنف يرث من ذاك الصنف؟ | issubclass(Developer, Employee) → True |
super().__init__(...) في صنف وارث يحتاج تهيئة إضافية فوق تهيئة الأب، هذا يجعلك تعيد كتابة كل شيء من الصفر بدل الاستفادة من عمل الصنف الأب.
اختبر فهمك: لو أضفت صنف Intern(Employee) بدون تعريف calculate_bonus() بنفسك، ماذا يحدث عند استدعائها؟
يُستخدَم تلقائيًا تعريف calculate_bonus() الموروث من Employee مباشرة (5%)، لأن الوراثة تمنحك أي ميثود لم تُعِد تعريفه صراحة في الصنف الابن.
مثال محلول
Employee براتب ثابت، وتريد Manager يأخذ الراتب نفسه ومكافأة. الجواب: class Manager(Employee) وفيه def pay(self): return super().pay() + self.bonus. super() تعيد استخدام حساب الأب بدل نسخه.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الوراثة هي الطريقة الوحيدة لإعادة استخدام الكود. | كثيرًا ما يكون الاحتواء أفضل: صنف يحمل كائنًا من صنف آخر. اسأل «هل هو نوع من؟» قبل أن ترث. |
| إعادة تعريف الطريقة تمسح طريقة الأب. | طريقة الأب باقية، والابن يستطيع ندائها بـsuper() ثم يضيف عليها. |
| التعدد الشكلي مفهوم نظري للمقابلات فقط. | تستخدمه كل يوم: len() تعمل على النص والقائمة والقاموس، وكل نوع يجيب بطريقته. |
جرّب بنفسك
class Dog(Animal) لا تعرّف speak. ماذا يحدث عند Dog().speak()؟
عندما لا يجد بايثون الطريقة في الصنف نفسه يبحث عنها في الأب، ثم في أب الأب، وهكذا. هذه هي الوراثة عمليًّا: ما لم تغيّره يبقى كما ورثته.
بطاقات مراجعة سريعة
تحقق من فهمك
ماذا تفعل super().__init__(name)؟
تنادي تهيئة الأب ليجهّز ما يخصّه، ثم يكمل الابن ما يخصّه.
صنف Dog(Animal) لم يعرّف eat. ماذا يحدث عند dog.eat()؟
يُنفَّذ eat الخاص بـAnimal، لأن البحث يصعد إلى الأب.
كيف تعرف إن كان كائن من نوع معيّن أو أحد أبنائه؟
بـisinstance(obj, Animal)، وهي تعيد True للأبناء أيضًا.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.