أساسيات بايثون 37: شرح تعدد الأشكال للمبتدئين

شرح تعدد الأشكال في بايثون باستخدام الكلاسات والوراثة للمبتدئين

بعد أن تعلمت معنى class وobject والوراثة، ستلاحظ أن عدة أنواع من الكائنات قد تشترك في سلوك عام، لكن كل نوع ينفذه بطريقته. القط والكلب والطائر كلها حيوانات، ويمكن أن نطلب من كل واحد إصدار صوت، لكن النتيجة تختلف. هذه الفكرة تسمى تعدد الأشكال.

المقصود ليس أن نكتب دوال كثيرة بأسماء مختلفة، بل أن نستخدم اسم دالة موحدًا مثل make_sound() أو move()، ثم يدع كل كائن ينفذ السلوك المناسب لنوعه.

{alertInfo} الفكرة ببساطة: عند استدعاء object.make_sound()، قد يطبع القط "مواء" والكلب "نباح" والطائر "زقزقة". اسم الدالة واحد، لكن النتيجة تعتمد على نوع الكائن.

{getToc} $title={محتوى المقال}

ما هو تعدد الأشكال في بايثون؟

تعدد الأشكال، أو Polymorphism، هو استخدام واجهة أو استدعاء موحد مع أنواع مختلفة من الكائنات. كل كائن يستجيب بما يناسبه، بينما يبقى الكود الذي يستدعي الدالة بسيطًا.

سبق أن استخدمت فكرة مشابهة حتى قبل دراسة الكلاسات. انظر إلى len():

print(len("بايثون"))
print(len([10, 20, 30]))
print(len({"name": "علي", "score": 90}))

الدالة len() تعمل مع النص والقائمة والقاموس، لكنها تعيد العدد المناسب لكل نوع. في الكلاسات سنطبق المبدأ نفسه، لكننا سنصمم الدوال بأنفسنا.

قبل البدء

هذا الدرس يبني على الدروس السابقة في البرمجة الكائنية، لذلك راجعها عند الحاجة:

أول مثال: حيوانات تصدر أصواتًا مختلفة

سننشئ كلاسًا أبًا باسم 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())
مثال تعدد الأشكال باستخدام كلاس Animal وكلاسات Cat وDog في بايثون

مثال عملي: حساب راتب أنواع مختلفة من الموظفين

هذا المثال أقرب إلى تطبيقات العمل. الموظف بدوام كامل والموظف بالساعات كلاهما يحتاج إلى دالة 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(). يختلف التنفيذ، لكن طريقة الاستخدام تظل واضحة.

هل تحتاج إلى الوراثة دائمًا؟

الوراثة طريقة سهلة لفهم تعدد الأشكال، لكنها ليست الطريق الوحيد في بايثون. أحيانًا يكفي أن يوفّر الكائن الدالة التي تحتاجها، حتى لو لم يرث من كلاس مشترك.

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().

{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().

تشغيل الدالة نفسها على قائمة من الكائنات المختلفة باستخدام حلقة for في بايثون

الفرق بين تعديل الدالة الموروثة وتعدد الأشكال

المفهوم معناه مثال
تعديل الدالة الموروثة الكلاس الابن يكتب دالة بالاسم نفسه الموجود في الكلاس الأب ليغير تنفيذها. Dog.make_sound() بدل النسخة العامة داخل Animal.
تعدد الأشكال نستخدم الاستدعاء نفسه مع كائنات مختلفة، فتظهر نتيجة مناسبة لكل نوع. حلقة تستدعي make_sound() على قط وكلب وطائر.

أخطاء شائعة

1. تغيير اسم الدالة في كل كلاس

إذا كانت دالة القط اسمها meow() ودالة الكلب اسمها bark()، فلن تملك واجهة موحدة. في المثال نستخدم make_sound() للجميع، ثم نغير التنفيذ الداخلي فقط.

2. اختلاف معلمات الدالة بلا سبب

حاول أن تكون الدوال المتشابهة متناسقة. إذا كانت send() تستقبل message في كلاس، فلا تجعلها تحتاج قيمًا مختلفة تمامًا في كلاس آخر إلا عند وجود سبب واضح في تصميم المشروع.

3. كتابة شروط كثيرة لفحص النوع

عندما تكتب سلسلة طويلة من isinstance() فقط لتحديد السلوك، فقد تكون الكلاسات بحاجة إلى دالة مشتركة. لا يعني ذلك أن isinstance() ممنوعة، لكنها ليست الحل الأول دائمًا.

4. استخدام super دون حاجة

استخدم super() عندما تحتاج فعلًا إلى جزء من سلوك الأب. أما إذا كان الكلاس الابن يملك تنفيذًا مختلفًا بالكامل، فلا تحتاج إلى استدعاء دالة الأب.

5. الخلط بين الكلاس والكائن

الكلاس هو القالب مثل Dog، أما الكائن فهو نسخة مثل dog = Dog(). نستدعي الدالة على الكائن: dog.make_sound().

استخدام super مع تعديل دالة موروثة في بايثون لتوضيح تعدد الأشكال

تمرين عملي

أنشئ كلاس أبًا باسم 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(). لكن الوراثة مفيدة عندما توجد علاقة حقيقية وواضحة بين الأنواع.

إرسال تعليق

أحدث أقدم