تعدد الأشكال (Polymorphism) في Python يعني استخدام الدالة أو الواجهة نفسها مع كائنات أو أنواع مختلفة، بحيث ينفذ كل منها السلوك المناسب له. مثلًا يمكن استدعاء make_sound() على قط وكلب وطائر، فيبقى اسم الدالة واحدًا بينما تختلف النتيجة حسب الكائن.
تظهر الفكرة في بايثون بأكثر من صورة، منها الدوال المدمجة التي تعمل مع أنواع مختلفة، وMethod Overriding مع الوراثة، وDuck Typing الذي يسمح باستخدام كائنات غير مترابطة ما دامت توفر السلوك المطلوب.
{alertInfo}
الفكرة ببساطة: عند استدعاء object.make_sound()، قد يطبع القط "مواء" والكلب "نباح" والطائر "زقزقة". اسم الدالة واحد، لكن النتيجة تعتمد على نوع الكائن.
{getToc} $title={محتوى المقال}
ما هو تعدد الأشكال في بايثون؟
تعدد الأشكال، أو Polymorphism، هو أن يبقى الاستدعاء موحدًا بينما يتغير السلوك بحسب الكائن أو نوع البيانات. لذلك لا يقتصر المفهوم على الكلاسات والوراثة فقط؛ يمكن أن يظهر أيضًا في الدوال المدمجة والمعاملات.
سبق أن استخدمت فكرة مشابهة حتى قبل دراسة الكلاسات. انظر إلى len():
print(len("بايثون"))
print(len([10, 20, 30]))
print(len({"name": "علي", "score": 90}))
الدالة len() تعمل مع النص والقائمة والقاموس، لكنها تعيد العدد المناسب لكل نوع. وبالمثل، يتغير سلوك المعامل + حسب البيانات: فهو يجمع الأرقام مثل 5 + 3، بينما يدمج النصوص مثل "Py" + "thon".
| الصورة الشائعة | كيف يظهر تعدد الأشكال؟ |
|---|---|
| دوال ومعاملات بايثون | الدالة أو المعامل نفسه يعمل مع أنواع مختلفة، مثل len() و+. |
Method Overriding |
كلاسات أبناء تعيد تعريف الدالة الموروثة وتنفذها بطرق مختلفة. |
Duck Typing |
كائنات قد لا ترث من أب مشترك، لكن يمكن استخدامها بالطريقة نفسها إذا وفرت الدالة المطلوبة. |
سنركز في الأمثلة التالية على تعدد الأشكال بين الكائنات، لأنه الأكثر ارتباطًا بالكلاسات والوراثة في هذا الجزء من كورس أساسيات بايثون.
قبل البدء
هذا الدرس يبني على الدروس السابقة في البرمجة الكائنية، لذلك راجعها عند الحاجة:
- أساسيات بايثون 34: شرح الكلاس والكائنات
- أساسيات بايثون 35: الفرق بين خصائص الكلاس وخصائص الكائن
- أساسيات بايثون 36: شرح الوراثة للمبتدئين
أول مثال: حيوانات تصدر أصواتًا مختلفة
سننشئ كلاسًا أبًا باسم Animal، ثم نرث منه ثلاثة كلاسات: Cat وDog وBird. كل كلاس ابن يعيد تعريف الدالة make_sound() بطريقته.
class Animal:
def make_sound(self):
print("صوت عام لحيوان")
class Cat(Animal):
def make_sound(self):
print("مواء")
class Dog(Animal):
def make_sound(self):
print("نباح")
class Bird(Animal):
def make_sound(self):
print("زقزقة")
cat = Cat()
dog = Dog()
bird = Bird()
cat.make_sound()
dog.make_sound()
bird.make_sound()
الناتج:
مواء
نباح
زقزقة
الاستدعاء واحد في كل مرة: make_sound(). لكن بايثون تنفذ الدالة الموجودة في الكلاس الحقيقي للكائن. كائن القط ينفذ نسخة Cat، وكائن الكلب ينفذ نسخة Dog.
تعديل الدالة الموروثة
في المثال السابق، كتبت الكلاسات الأبناء دالة بالاسم نفسه الموجود في الكلاس الأب. هذا يسمى تعديل الدالة الموروثة أو Method Overriding.
| الكلاس | الدالة الموجودة داخله | النتيجة عند الاستدعاء |
|---|---|---|
Animal |
make_sound() |
صوت عام لحيوان. |
Cat |
make_sound() |
مواء. |
Dog |
make_sound() |
نباح. |
Bird |
make_sound() |
زقزقة. |
تعديل الدالة الموروثة ليس هو تعدد الأشكال نفسه، لكنه طريقة مهمة للوصول إليه: نكتب سلوكًا خاصًا لكل كلاس، ثم نستفيد من الاسم الموحد للدالة عند استخدام الكائنات معًا.
القائمة الواحدة: أين تظهر الفائدة الحقيقية؟
بدل استدعاء كل كائن يدويًا، ضع الكائنات المختلفة داخل قائمة ثم استخدم حلقة واحدة:
animals = [Cat(), Dog(), Bird()]
for animal in animals:
animal.make_sound()
الناتج يبقى:
مواء
نباح
زقزقة
الحلقة لا تحتاج إلى معرفة هل الكائن قط أو كلب أو طائر؛ فهي تعرف أن كل عنصر يجب أن يقدم الدالة make_sound(). وإذا أضفت كلاسًا جديدًا باسم Horse يملك الدالة نفسها، فلن تحتاج إلى تعديل الحلقة.
class Horse(Animal):
def make_sound(self):
print("صهيل")
animals.append(Horse())
مثال عملي: حساب راتب أنواع مختلفة من الموظفين
هذا المثال أقرب إلى تطبيقات العمل. الموظف بدوام كامل والموظف بالساعات كلاهما يحتاج إلى دالة calculate_salary()، لكن طريقة الحساب مختلفة.
class Employee:
def calculate_salary(self):
raise NotImplementedError(
"يجب أن يكتب كل نوع موظف طريقة حساب راتبه."
)
class FullTimeEmployee(Employee):
def __init__(self, monthly_salary):
self.monthly_salary = monthly_salary
def calculate_salary(self):
return self.monthly_salary
class HourlyEmployee(Employee):
def __init__(self, hours, hourly_rate):
self.hours = hours
self.hourly_rate = hourly_rate
def calculate_salary(self):
return self.hours * self.hourly_rate
employees = [
FullTimeEmployee(900),
HourlyEmployee(30, 12),
]
for employee in employees:
print(employee.calculate_salary())
الناتج:
900
360
لاحظ أن الحلقة تستدعي calculate_salary() فقط، ولا تحتاج إلى استخدام سلسلة شروط لمعرفة نوع الموظف. كل كلاس مسؤول عن تنفيذ حسابه الخاص.
مثال آخر: وسائل نقل تتحرك بأساليب مختلفة
class Vehicle:
def move(self):
print("المركبة تتحرك")
class Car(Vehicle):
def move(self):
print("السيارة تسير على الطريق")
class Boat(Vehicle):
def move(self):
print("القارب يبحر في الماء")
class Plane(Vehicle):
def move(self):
print("الطائرة تطير في الجو")
vehicles = [Car(), Boat(), Plane()]
for vehicle in vehicles:
vehicle.move()
هنا الدالة الموحدة هي move(). يختلف التنفيذ، لكن طريقة الاستخدام تظل واضحة.
هل يحتاج Polymorphism إلى الوراثة؟ Duck Typing في Python
لا. الوراثة طريقة شائعة لتحقيق تعدد الأشكال، لكنها ليست شرطًا في بايثون. عندما يهتم الكود بالسلوك الذي يوفره الكائن بدل نوعه أو شجرة الوراثة الخاصة به، يسمى هذا الأسلوب Duck Typing.
بمعنى عملي: إذا كانت الدالة تحتاج إلى كائن يملك send()، فيمكنها استخدام أي كائن يوفر هذه الدالة، حتى لو لم يرث من كلاس أب مشترك.
class EmailNotification:
def send(self, message):
print(f"إرسال بريد إلكتروني: {message}")
class ConsoleNotification:
def send(self, message):
print(f"عرض في الطرفية: {message}")
def notify(channel, message):
channel.send(message)
notify(EmailNotification(), "تم حفظ التقرير.")
notify(ConsoleNotification(), "تم حفظ التقرير.")
الدالة notify() لا تسأل عن النوع. يكفي أن الكائن الذي تستقبله يوفر الدالة send(). هذا مثال مباشر على Duck Typing وتعدد الأشكال بدون وراثة.
{alertWarning} تنبيه: لا تجعل كل الكلاسات في مشروعك ترث من كلاس أب فقط لأنك تريد دالة مشتركة. استخدم الوراثة عندما توجد علاقة منطقية فعلًا، مثل القط نوع من الحيوان. أما الكائنات غير المرتبطة فقد يكفي أن تتفق على السلوك المطلوب.
استخدام super عند تعديل الدالة
أحيانًا لا تريد استبدال سلوك الكلاس الأب بالكامل. تريد تشغيل جزء منه ثم إضافة تفاصيل خاصة بالكلاس الابن. هنا تستخدم super().
class Person:
def introduce(self):
print("مرحبًا، أنا شخص.")
class Student(Person):
def introduce(self):
super().introduce()
print("وأنا أتعلم بايثون.")
student = Student()
student.introduce()
الناتج:
مرحبًا، أنا شخص.
وأنا أتعلم بايثون.
الكلاس Student عدّل الدالة introduce()، لكنه احتفظ بسلوك الأب أولًا عبر super().introduce().
الفرق بين الوراثة وMethod Overriding وتعدد الأشكال
| المفهوم | معناه | مثال |
|---|---|---|
الوراثة Inheritance |
إنشاء كلاس ابن يستفيد من خصائص ودوال كلاس أب، مع إمكانية إضافة أو تعديل سلوك. | Dog يرث من Animal. |
تعديل الدالة Method Overriding |
الكلاس الابن يكتب دالة بالاسم نفسه الموجود في الكلاس الأب ليقدم تنفيذًا خاصًا به. | Dog.make_sound() يعيد تعريف الدالة الموروثة من Animal. |
تعدد الأشكال Polymorphism |
نستخدم الاستدعاء نفسه مع كائنات مختلفة، فينفذ كل كائن السلوك المناسب له. | حلقة تستدعي make_sound() على قط وكلب وطائر. |
إذن الوراثة تبني العلاقة بين الكلاسات، وMethod Overriding يغير تنفيذ دالة موروثة، أما تعدد الأشكال فهو الفائدة التي تسمح لك باستخدام الاستدعاء نفسه مع كائنات مختلفة. ويمكن تحقيق تعدد الأشكال في بايثون أيضًا بدون وراثة عبر Duck Typing.
أخطاء شائعة
1. تغيير اسم الدالة في كل كلاس
إذا كانت دالة القط اسمها meow() ودالة الكلب اسمها bark()، فلن تملك واجهة موحدة. في المثال نستخدم make_sound() للجميع، ثم نغير التنفيذ الداخلي فقط.
2. اختلاف معلمات الدالة بلا سبب
حاول أن تكون الدوال المتشابهة متناسقة. إذا كانت send() تستقبل message في كلاس، فلا تجعلها تحتاج قيمًا مختلفة تمامًا في كلاس آخر إلا عند وجود سبب واضح في تصميم المشروع.
3. كتابة شروط كثيرة لفحص النوع
عندما تكتب سلسلة طويلة من isinstance() فقط لتحديد السلوك، فقد تكون الكلاسات بحاجة إلى دالة مشتركة. لا يعني ذلك أن isinstance() ممنوعة، لكنها ليست الحل الأول دائمًا.
4. استخدام super دون حاجة
استخدم super() عندما تحتاج فعلًا إلى جزء من سلوك الأب. أما إذا كان الكلاس الابن يملك تنفيذًا مختلفًا بالكامل، فلا تحتاج إلى استدعاء دالة الأب.
5. الخلط بين الكلاس والكائن
الكلاس هو القالب مثل Dog، أما الكائن فهو نسخة مثل dog = Dog(). نستدعي الدالة على الكائن: dog.make_sound().
تمرين عملي
أنشئ كلاس أبًا باسم Shape يحتوي على دالة get_area(). ثم أنشئ كلاسَين:
Rectangleيحسب المساحة من الطول والعرض.Squareيحسب المساحة من طول الضلع.
ضع كائنات من النوعين داخل قائمة، ثم استخدم حلقة واحدة لطباعة مساحة كل شكل عبر get_area(). ويمكنك تجربة الحل مباشرة في محرر بايثون أونلاين قبل مشاهدة الحل المقترح.
حل مقترح
class Shape:
def get_area(self):
raise NotImplementedError(
"يجب أن يكتب كل شكل طريقة حساب مساحته."
)
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def get_area(self):
return self.side * self.side
shapes = [
Rectangle(5, 3),
Square(4),
]
for shape in shapes:
print(shape.get_area())
الناتج:
15
16
الخلاصة
- تعدد الأشكال يعني استخدام استدعاء موحد مع كائنات مختلفة، فتظهر نتيجة مناسبة لكل نوع.
- يمكن أن تملك عدة كلاسات دالة بالاسم نفسه مثل
make_sound()أوmove(). - تعديل دالة موروثة داخل الكلاس الابن طريقة شائعة لإنشاء سلوك مختلف.
- تظهر الفائدة عندما تضع كائنات مختلفة في قائمة واحدة وتستدعي الدالة نفسها داخل حلقة.
- يساعد
super()على استدعاء جزء من سلوك الكلاس الأب عند الحاجة. - استخدم الوراثة عندما توجد علاقة منطقية، ولا تعتمد على فحص الأنواع بالشروط عندما يمكن أن توفر دالة موحدة.
{alertSuccess} قاعدة عملية: عندما تجد نفسك تسأل عن نوع الكائن كل مرة قبل تنفيذ السلوك، فكّر: هل يمكن أن أضع هذا السلوك داخل دالة موحدة مثلsend()أوget_area()وأدع كل كلاس ينفذها بطريقته؟
مصادر رسمية للتوسع
أسئلة شائعة
ما معنى تعدد الأشكال في بايثون؟
هو استخدام الدالة أو الواجهة نفسها مع كائنات مختلفة، بحيث ينفذ كل كائن السلوك المناسب لنوعه. مثل تشغيل make_sound() على قط وكلب وطائر.
هل تعدد الأشكال هو نفسه الوراثة؟
لا. الوراثة تسمح لك بإنشاء كلاس ابن يستفيد من كلاس أب. أما تعدد الأشكال فيتعلق باستخدام واجهة أو دالة موحدة مع كائنات قد تنفذها بطرق مختلفة. الوراثة طريقة شائعة للوصول إليه، لكنها ليست الوحيدة في بايثون.
ما هو تعديل الدالة الموروثة؟
هو كتابة دالة داخل الكلاس الابن بالاسم نفسه الموجود في الكلاس الأب، حتى يوفر الابن تنفيذًا أكثر تحديدًا. مثل كتابة make_sound() داخل Dog بدل استخدام النسخة العامة في Animal.
متى أستخدم super داخل دالة معدلة؟
استخدمها عندما تحتاج إلى تشغيل جزء من الدالة الموجودة في الكلاس الأب ثم إضافة سلوك خاص بالكلاس الابن. لا تحتاج إليها عندما تريد استبدال السلوك بالكامل.
هل يجب أن ترث كل الكلاسات من كلاس واحد؟
لا. في بايثون يمكن لدالة أن تعمل مع أي كائن يوفّر السلوك الذي تحتاجه، مثل كائن لديه دالة send(). هذا هو الأساس العملي لفكرة Duck Typing، بينما تكون الوراثة مفيدة عندما توجد علاقة حقيقية وواضحة بين الأنواع.
ما أشهر صور تعدد الأشكال في Python؟
من الصور الشائعة: عمل الدوال والمعاملات مع أنواع مختلفة مثل len() و+، واستخدام Method Overriding مع الوراثة، واستخدام Duck Typing مع كائنات لا تحتاج إلى أب مشترك.



