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

ما هو تعدد الأشكال (Polymorphism)؟

في البرمجة، تعدد الأشكال (polymorphism) هو قدرة جزء واحد من الكود، مثل استدعاء دالة أو اسم دالة عضو، على العمل مع قيم من أنواع مختلفة، بحيث يقدّم كل نوع سلوكه الخاص. فاستدعاء shape.area() يُجري حسابًا للدائرة يختلف عن حسابه للمربع.

بقلم Kevin Spektor, شريك مؤسس ورئيس قسم التكنولوجيا (CTO)

آخر تحديث 24 سبتمبر 2026

في بايثون، تُرجع len("hello") القيمة 5، وتُرجع len([4, 8, 15]) القيمة 3، وتُرجع len({"a": 1}) القيمة 1. إنه اسم دالة واحد يُستخدم مع ثلاثة أنواع، وكل نوع يعدّ بطريقته: النص يعدّ الحروف، والقائمة تعدّ العناصر، والقاموس يعدّ المفاتيح. والكلمة الإنجليزية من اليونانية: poly أي كثير، وmorph أي شكل. استدعاء واحد، وأشكال كثيرة من السلوك.

كيف يعمل تعدد الأشكال

يستدعي الكود دالة باسمها، مثل shape.area(). وعندما يُنفَّذ هذا السطر، تنظر اللغة إلى الكائن الفعلي وتجد area في صنف الكائن نفسه. يسمى هذا البحث الإرسال الديناميكي (dynamic dispatch)، وهو ما يتيح لسطر واحد أن يقوم بعمل مختلف لكائنات مختلفة:

Circle 3.14
Square 9
Circle 12.57

تجد بايثون الدالة بالبحث في صنف الكائن أولًا، ثم في أصنافه الأب بترتيب ثابت يسمى ترتيب حل الدوال (method resolution order). لو لم يكن لـ Square دالة area() خاصة به، لوصل البحث إلى Shape.area() ورفع NotImplementedError، وهي إشارة إلى أن كل صنف فرعي يجب أن يقدّم دالته.

لا تفحص الحلقة أبدًا نوع الشكل الذي تحمله. ولإضافة مثلث، تكتب صنف Triangle بدالة area() خاصة به، فتعمل الحلقة دون تغيير. ودون تعدد الأشكال ستحتاج الحلقة إلى فرع if isinstance(...) لكل شكل، يُعدَّل كلما ظهر شكل جديد.

أنواع تعدد الأشكال

النوعيسمى أيضًاكيف يبدومتى تُختار النسخة
الأنواع الفرعية (subtype)تعدد الأشكال وقت التشغيل، إعادة تعريف الدوال (overriding)صنف فرعي يستبدل دالة من صنفه الأبأثناء تشغيل البرنامج
المخصص (ad hoc)التحميل الزائد (overloading)اسم واحد بعدة نسخ لأنواع معاملات مختلفة، ويشمل أيضًا التحميل الزائد للعواملوقت الترجمة في جافا و C++
البارامتري (parametric)الأنواع العامة (generics)، القوالب (templates)كود واحد يعمل مع أي نوع: List<T> في جافا، وvector<T> في C++وقت الترجمة

حين يقول الناس "تعدد الأشكال" في البرمجة كائنية التوجه دون تفصيل، فإنهم يقصدون في الغالب الصف الأول: تعدد أشكال الأنواع الفرعية عبر إعادة التعريف.

في بايثون الأنواع الثلاثة كلها بطريقتها الخاصة. إعادة التعريف تعمل كما في مثال الأشكال. والعوامل مثل + تعدد أشكال مخصص. ودوال مثل max() وsorted() بارامترية في روحها: فهي تقبل قائمة من الأرقام أو النصوص أو التواريخ، ما دامت العناصر قابلة للمقارنة، ويمكن لتلميحات الأنواع مثل list[int] أو TypeVar أن تنص على ذلك صراحةً.

تعدد الأشكال وقت الترجمة ووقت التشغيل في جافا

في جافا النوعان كلاهما، وأسئلة المقابلات كثيرًا ما تسأل عن الفرق بينهما:

class Animal { String sound() { return "..."; } }
class Dog extends Animal {
    @Override String sound() { return "Woof"; }
}

Animal pet = new Dog();
pet.sound();          // "Woof": chosen at run time from the object

int add(int a, int b)          { return a + b; }
double add(double a, double b) { return a + b; }
add(2, 3);            // int version, chosen by the compiler
add(2.5, 1.0);        // double version

المتغير pet من النوع Animal، ومع ذلك تُنفَّذ نسخة Dog، لأن الاختيار يعتمد على الكائن لا على المتغير. أما الدالتان add فهما تحميل زائد: يختار المترجم إحداهما من أنواع الوسائط قبل تشغيل البرنامج. والواجهات تعطي سلوك وقت التشغيل نفسه دون مشاركة الكود: فأي صنف يطبّق Comparable يمكن ترتيبه بـ Collections.sort. وتعمل C++ بالطريقة نفسها مع فرق واحد: لا تحصل الدالة على الإرسال وقت التشغيل إلا إذا صُرّح عنها بـ virtual. راجع تعدد الأشكال في جافا، والتحميل الزائد للدوال، والدوال الافتراضية في C++.

تعدد الأشكال في بايثون: duck typing والعوامل

لا تشترط بايثون صنفًا أبًا مشتركًا. إن كان للكائن الدالة التي يستدعيها الكود، فإنه يعمل: "إن كان يمشي كالبطة ويبطبط كالبطة، فهو بطة". والعوامل متعددة الأشكال أيضًا. كتابة a + b تستدعي a.__add__(b)، فتستطيع أصنافك الخاصة دعم + وlen() وprint():

3 abcd [1, 2]
Vector(11, 22)
5 3 2

الوراثة طريقة واحدة للحصول على تعدد الأشكال، وليست شرطًا. فالصنف Vector لا يرث من شيء خاص، ومع ذلك تتعامل معه len() و+ كأنه نوع مدمج. ويُظهر توثيق الوراثة في بايثون طريق الأصناف الفرعية.

أخطاء شائعة

توقُّع أن تدعم بايثون التحميل الزائد بحسب التوقيع. تعريف def ثانٍ بالاسم نفسه يستبدل الأول بصمت:

Hi Ana Silva
Traceback (most recent call last):
  ...
TypeError: greet() missing 1 required positional argument: 'last'

استخدم وسيطًا افتراضيًا بدلًا من ذلك: def greet(first, last="").

نسيان virtual في C++. دونها، استدعاء دالة عبر مؤشر إلى الصنف الأساسي يُنفّذ نسخة الصنف الأساسي، حتى لو كان الكائن من صنف فرعي.

إعادة تعريف تغيّر المعنى. إن أرجعت area() في صنف فرعي ما المحيطَ بدل المساحة، يتعطل كل مستدعٍ يثق بـ area(). يجب أن تحافظ إعادة التعريف على الوعد الذي قطعه الأب، وتُعرف هذه القاعدة بمبدأ استبدال Liskov.

فحوص الأنواع في كل مكان. سلسلة من فروع if type(x) == ... علامة في الغالب على أن كل نوع يجب أن يحمل دالته الخاصة بدلًا من ذلك.

إلى أين بعد ذلك

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

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

ما هو مثال على تعدد الأشكال؟
العامل + في بايثون: 1 + 2 يجمع الأرقام، و"ab" + "cd" يضم النصوص، و[1] + [2] يضم القوائم. الرمز نفسه يعمل على ثلاثة أنواع، وكل نوع يقرر معناه. وفي الكود كائني التوجه، المثال الكلاسيكي قائمة من الأشكال يجيب كل منها عن area() بصيغته الخاصة.
ما الفرق بين تعدد الأشكال والوراثة؟
الوراثة تتيح للصنف إعادة استخدام كود صنف أب. أما تعدد الأشكال فيتيح للكود التعامل مع أنواع مختلفة عبر واجهة مشتركة واحدة. الوراثة طريقة شائعة للحصول على تعدد الأشكال، لأن الصنف الفرعي يستطيع إعادة تعريف دالة الأب، لكن duck typing في بايثون والواجهات (interfaces) في جافا تعطي تعدد الأشكال دون مشاركة أي كود.
ما هو تعدد الأشكال في علم الأحياء؟
في علم الأحياء، تعدد الأشكال يعني وجود شكلين أو أكثر لصفة أو جين في المجموعة السكانية نفسها، مثل فصائل الدم A و B و AB و O عند البشر. يشترك مع المصطلح البرمجي في الجذر اليوناني "أشكال كثيرة"، لكن المفهومين لا علاقة بينهما.
هل تدعم بايثون التحميل الزائد للدوال (overloading)؟
ليس بحسب أنواع المعاملات أو عددها كما تفعل جافا و C++. فتعريف def ثانٍ بالاسم نفسه يستبدل الأول. ويحصل كود بايثون على الأثر نفسه باستخدام الوسائط الافتراضية، أو *args، أو functools.singledispatch التي تختار دالة بحسب نوع الوسيط الأول.
لماذا تعدد الأشكال مفيد؟
لأنه يتيح لك إضافة أنواع جديدة دون تغيير الكود الذي يستخدمها. برنامج الرسم الذي يستدعي shape.draw() على كل شكل لا يحتاج إلى أي تعديل حين تضيف صنفًا للمثلث، فلا يُكتب إلا الصنف الجديد. ودون تعدد الأشكال ستنمو تلك الحلقة بفرع if جديد لكل نوع.
Coddy programming languages illustration

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

ابدأ الآن