البرمجة الموجهة للكائنات OOP في Python - جزء4 تعدد الأشكال Polymorphism
أحد المبادئ الرئيسية في في البرمجة الموجهة للكائنات OOP والذي يمنح الكود مرونة عالية ، مبدأ تعدد الأشكال (Polymorphism)... فماذا يعني تعدد الأشكال Polymorphism في البرمجة….!! وكيف يتحقق في لغة البرمجة Python….؟ّ!!!
يُعد تعدد الأشكال Polymorphism أحد المبادئ الأساسية في البرمجة الكائنية OOP، حيث يسمح لنا بكتابة أكواد عامة يمكنها التعامل مع أنواع متعددة من الكائنات. يُنفذ تعدد الأشكال في بايثون بطرق متعددة، مثل استخدام الدوال التي يمكن تطبيقها على كائنات مختلفة، وهو ما يسهل عملية تطوير البرامج ويعزز قابليتها للتوسع والصيانة. في هذا المقال، سنستعرض مبدأ تعدد الأشكال (Polymorphism) في بايثون، ونوضح كيف يمكن استغلاله لكتابة أكواد أكثر كفاءة وفعالية، لنبدأ…
نتعرف في هذا المقال على:
- ما هو تعدد الأشكال Polymorphism في البرمجة؟
- تعدد الأشكال Polymorphism و الـ Methods Overloading في Python.
- تعدد الأشكال Polymorphism مع البرمجة الموجهة للكائنات OOP.
ما هو تعدد الأشكال Polymorphism في البرمجة؟
تعني كلمة تعدد الأشكال Polymorphism توفر أشكال عدة لغرض واحد. مثلاً الهواتف الذكية أو الشخصية تعدد أشكالها بحسب نوع الجهاز ولكن جميعها هواتف تؤدي نفس المهام. أما في البرمجة فإن تعدد الأشكال (Polymorphism) يعني إجراء نفس الوظيفة بنفس الاسم ولكن بطريقة تنفيذ مختلفة في كل مرة. بمعنى أدق إن تعدد الأشكال Polymorphism في البرمجة الموجهة للكائنات يشير إلى قدرة واجهة واحدة على أن تُستخدم للإشارة إلى تنفيذات متعددة لسلوك معين.
وقد تم استعارة مبدأ تعدد الأشكال Polymorphism من مبدأ في علم الأحياء حيث يمكن للكائن الحي أو النوع أن يكون له أشكال أو مراحل مختلفة عديدة. ويعني تعدد الأشكال (Polymorphism) في البرمجة الموجهة للكائنات OOP توفير واجهة واحدة للكيانات من أنواع مختلفة من التنفيذ. ويتوفر نوعان من تعدد الأشكال Polymorphism:
1. تعدد الأشكال الديناميكي (Dynamic Polymorphism):
ويعرف أيضًا باسم تعدد أشكال وقت التشغيل (Run-Time Polymorphism)، يتحقق تعدد الأشكال الديناميكي (Dynamic Polymorphism)، أثناء التنفيذ (قت التشغيل)، ما قد يعطي تأثير سلبياً على الأداء. ومن الأمثلة عليه مفهوم اعادة الكتابة Overriding، حيث توفر الفئة الفرعية تنفيذًا محددًا لطريقة يتم توفيرها بالفعل بواسطة فئتها الأصلية. (المزيد في مقال الوارثة في Python ).
2.تعدد الأشكال الثابت (Static Polymorphism):
ويعرف أيضًا باسم تعدد أشكال وقت التجميع (Compile-Time Polymorphism)، يتحقق تعدد الأشكال الثابت (Static Polymorphism) أثناء تجميع الكود (وقت التجميع). ومن الأمثلة عليه التحميل الزائد للدوال (Methods Overloading) إحدى ميزات البرمجة الموجهة للكائنات OOP حيث يمكن أن يحتوي الـ Class على دوال متعددة بنفس الاسم ولكن بمعلمات (parameters) مختلفة. لزيادة تحميل الدوال يجب علينا تغيير عدد أو نوع المعلمات، أو كليهما.
وسواء كان تعدد الأشكال Polymorphism من النوع الثابث أو الديناميكي ، فإن تعدد الأشكال يتيح لنا التركيز على ما يفعله الكائن (object) بدلاً من نوعه، مما يسهل تطوير وصيانة التطبيقات البرمجية. فمبدأ تعدد الأشكال (Polymorphism) في البرمجة بمثابة حجر الزاوية الذي يعزز من مرونة وفعالية الكود. من خلال السماح للكائنات المختلفة بمشاركة نفس الواجهات، يمكن للمبرمجين تصميم أنظمة أكثر تنظيماً وقابلية للتوسع.
تعدد الأشكال Polymorphism و الـ Methods Overloading في Python
من مفاهيم البرمجة الشهيرة التي تحقق مبدأ تعدد الأشكال Polymorphism التحميل الزائد للدوال (Methods Overloading) حيث يسمح بتعريف دوال متعددة بنفس الاسم ولكن بقوائم معلمات (parameters) مختلفة. يحدد المترجم (compiler) الدالة التي سيتم استدعاؤها بناءً على عدد وأنواع البيانات المقدمة Arguments أثناء استدعاء الدالة.
و على عكس لغات البرمجة الأخرى مثل Java وC++ وC# ، لا تدعم بايثون ميزة التحميل الزائد للدوال (Methods Overloading) بشكل افتراضي. (المزيد عن Methods Overloading مقال التعامل مع الدوال في لغة C#). ومع ذلك، يمكن تحقيق التحميل الزائد Overloading في بايثون بصور متعددة ، مثلاً بعض دوال بايثون المدمجة تعمل على التحميل الزائد (Overloading) كما يتضح معنا في المثال التالي مع الدالة المدمجة len():
#len function takes a string arguments
print (len("Hello"))
#len function takes a tuple arguments
print (len(('hi', 1, 2, 3)))
#len function takes a list arguments
print(len(['hi', 1, 2,3])) #len function takes a string arguments
print (len("Hello"))
#len function takes a tuple arguments
print (len(('hi', 1, 2,3)))
#len function takes a list arguments
print(len(['hi', 1, 2,3]))
كما نرى في المثال تنوع البيانات التي تعمل معها الدالة len() والتي لايقتصرعملها مع لـ string او List أو Tuple انما تعمل أيضاً مع القواميس Dictionaries.
ويمكن ايضاً تنفيذ التحميل الزائد للطرق باستخدام MultipleDispatch ،وهو ما يتم بواسطة استخدام دالة الإرسال من وحدة خارجية تسمى MultipleDispatch لهذا الغرض. ولكن لابد أولاً، من تثبيت وحدة Multipledispatch باستخدام الأمر التالي:
pip install multipledispatch
تحتوي هذه الوحدة على @dispatch وهو يأخذ نوع وعدد الـ Arguments التي سيتم تمريرها إلى الدالة المراد عمل الـOverloading لها . المثال التالي يوضح طريقة استخدام @dispatch للعمل Overloading للدالة Add() :
from multipledispatch import dispatch
class Addition:
@dispatch(int, int)
def Add(self, a, b):
x = a + b
return x
@dispatch(int, int, int)
def Add(self, a, b, c):
x = a + b + c
return x
obj = Addition()
print (obj.Add(1,2,3))
print (obj.Add(4,5))
وعلى الرغم من أن بايثون لا تدعم التحميل الزائد للدوال (Methods Overloading) بشكل مباشر مثل بعض اللغات الأخرى، إلا أنها تعوض عن ذلك بمرونة من خلال استخدام الوسائل المتاحة مثل القيم الافتراضية والمعالجة الشرطية. هذه القدرات تجعل من بايثون خيارًا مثاليًا لتصميم برامج تعتمد على تعدد الأشكال (Polymorphism ) بطرق مبتكرة وفعالة.
تعدد الأشكال Polymorphism مع البرمجة الموجهة للكائنات OOP
يمكن تحقيق مبدأ تعدد الأشكال Polymorphism بواسطة مفاهيم البرمجة الموجهة للكائنات بصور متعددة ومن خلال استخدام مفاهيم الـ OOP المختلفة فمثلاً:
- تعدد الأشكال Polymorphism مع Class & Object.
- تعدد الأشكال Polymorphism و الوراثة Inheritance.
- تعدد الأشكال Polymorphism و الواجهات Interfaces.
تعدد الأشكال Polymorphism مع Class & Object
يتحقق تعدد الأشكال Polymorphism في Python مع الـ Class & Object، من خلال كتابة كود يسمح لنا استخدام نفس الواجهة للتعامل مع أنواع مختلفة من الكائنات دون الحاجة لمعرفة نوع الكائن بشكل صريح. فعند استخدام الـ Classes المختلفة دوال بنفس الاسم وبتنفيذ مختلف وخاص لكل Class.
في مثال بسيط يوضح تعدد الأشكال Polymorphism في Python مع الـ Class & Object،يوضح المثال التالي اختلاف الـ Output لكل Object مع أن طريقة الاستدعاء نفسها :
class Cat:
def sound(self):
return "Meow"
class Dog:
def sound(self):
return "Woof"
def make_sound(animal):
print(animal.sound())
cat = Cat()
dog = Dog()
make_sound(cat) # Output: Meow
make_sound(dog) # Output: Woof
مع الـ Class & Object يمكن العمل مع المشغلات Operators، فمن خلال العمل مع المشغلات Operators وهي رموز خاصة تستخدم في لغة بايثون و تقوم بعمليات على القيم والمتغيرات. والمشغلات يمكنها تنفيذ عمليات حسابية و علائقية ومنطقية ، يأخذ تعريف المشغل الكلمة def ومن ثم اسم المشغل مسبوق ومتبوع بعلامتين من الشرطة السفلية.
لنرى طريقة استخدام المشغلات Operators لتحقيق تعدد الأشكال Polymorphism مع مشغل يقوم بجمع الاحداثيات لنقطتين :
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
#Create an operator for sum two points
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
#Creating two Point objects
point1 = Point(1, 2)
point2 = Point(3, 4)
# Adding two Point objects using the overloaded '+' operator
print( point1 + point2) # Output: (4, 6)
كما نرى في المثال المشغل __add__ قام بالتعرف على القيم الخاصة لكل Object ثم جمعها بصورة صحيحة.المزيد عن الـ Class & Object في جزء الـ Class & Object من مقالات البرمجة الموجهة للكائنات في Python.
تعدد الأشكال Polymorphism و الوراثة Inheritance
في Python، الوراثة (Inheritance) وتعدد الأشكال (Polymorphism) هما مبادئ أساسية في البرمجة الموجهة للكائنات تسمحان بزيادة قابلية إعادة الاستخدام وتنظيم كود. الوراثة تسمح لك بإنشاء Class جديد يستفيد من الصفات والوظائف الموجودة في Class آخر.
في Python، عندما ترث من Class (والذي يُعرف بالـ Class الوالد أو الـ Class الأساسي)، يمكننا إضافة سمات جديدة أو تعديل السمات الموجودة أو استدعاء الدوال من الـ Class الأساسي. (المزيد في مقال عن الوراثة Inheritance في Python) في المثال التي نرى اختلاف التنفيذ للدالة sound في كل فئة فرعية Subclass..
class Animal:
def sound(self):
pass
class Dog(Animal):
def sound(self):
return "Woof"
class Cat(Animal):
def sound(self):
return "Meow"
def make_sound(animal):
print(animal.sound())
dog = Dog()
cat = Cat()
make_sound(dog) # Output: Woof
make_sound(cat) # Output: Meow
كما رأينا في المثال السابق تم استدعاء الدالة sound بواسطة الدالة make_sound ، وهو مثال على الربط الديناميكي حيث يرتبط مفهوم الربط الديناميكي (Dynamic Binding) ارتباطًا وثيقًا مع الوراثة ليحقق تعدد الأشكال Polymorphism. والربط الديناميكي Dynamic Binding في بايثون هو عملية حل دالة أو سمة في وقت التشغيل، بدلاً من وقت التجميع.
فوفقًا لتعدد الأشكال Polymorphism تستجيب الكائنات المختلفة بشكل مختلف لنفس الاستدعاء. ويحقيق هذا السلوك من خلال Overriding للدالة حيث توفر الفئة الفرعية تنفيذها لطريقة محددة في فئتها الأساسية. وهنا يحدد الـ Complier الدالة أو السمة المناسبة لاستدعائه بناءً على نوع الكائن أو التسلسل الهرمي للـ class. وهذا يعني أن الطريقة أو السمة المحددة التي سيتم استدعاؤها يتم تحديدها ديناميكيًا، بناءً على النوع الفعلي للكائن. لنرى المثال التالي:
class AnimalsSounds:
def Sound(self):
print ("Sound")
return
class Cat(AnimalsSounds):
def Sound(self):
print ("Meow")
return
class Dog(AnimalsSounds):
def Sound(self):
print ("Woof")
return
Sounds = [Cat(), Dog()]
for animal in Sounds:
animal.Sound()
كما نرى في المثال تم استخدام الفئات الفرعية Subclasses كعناصر في قائمة List وتم الوصول لها بواسطة حلقة تكرارا تماماً كما يتم الوصول لعناصر القوائم. فيتم ربط Sound() ديناميكيًا بالتنفيذ المقابل بناءً على نوع الكائن. هذه هي الطريقة التي يتم بها تنفيذ الربط الديناميكي في بايثون.
تعمل الوراثة Inheritance بين الـ Classesعلى تحقيق مفاهيم متنوعة منها مفهوم اعادة الكتابة الـ Overriding و التجريد الـ Abstraction التي بدورها تحقق مبدأ تعدد الأشكال Polymorphism، ففي التجريد الـ Abstraction أو الـ Overriding يتم تعريف الدالة في الـ Subclass بنفس اسم دالة في الـ Class الأساسي.و يحدد مُفسِّر بايثون الدالة التي سيتم استدعاؤها في وقت التشغيل استنادًا إلى الـ Object الذي تم انشاءه. كلاً من هذه المفاهيم تم شرحها بالتفصيل مع الأمثلة عليها في مقال الوارثة في Python.
تعدد الأشكال Polymorphism و الواجهات Interfaces
في هندسة البرمجيات، الواجهة هي نمط معماري للبرمجيات. وفي لغات الموجهة للكائنات (OOP) مثل بايثون، الواجهة (interface) هي تشبه الفئة (Class) ولكن الدوال فيها تكون مجردة (Abstract) فتحتوي على تعريف النموذج الأولي بدون أي كود قابل للتنفيذ. أي يجب تنفيذ الوظيفة المطلوبة بواسطة دوال الفئات فرعية (Subclasses) التي تشتق من الواجهة interface الاساسية.
مما يعني أنه لكي يتسنى لنا تنفيذ واجهة Interface معينة علينا اشتقاق فئة واحدة منها على الأقل، ويكون لكل فئة فرعية للواجهة Interface طريقة تنفيذ خاصة بها، وهو ما يحقق مبدأ تعدد الأشكال (Polymorphism).
الواجهات Interfaces في بايثون
لتنفيذ الواجهات Interfaces في لغات مثل جافا وجو، توجد كلمة رئيسية تسمى (interface) والتي تستخدم لتعريف واجهة. لكن بايثون لا تحتوي على هذه الكلمة الرئيسية أو أي كلمة رئيسية مشابهة. انما يتم العمل مع الوجهات في بايثون باستخدام التجريد Abstraction ، تطرقنا الى مفهوم التجريد Abstraction في مقال عن الوراثة في بايثون.
تبدو الفئة المجردة (Abstract class) والواجهة (interface) متشابهتين في بايثون، الفرق الوحيد بينهما هو أن الفئة المجردة Abstract class قد تحتوي على بعض الدوال غير المجردة، بينما يجب أن تكون جميع الدوال في الواجهة interface مجردة، ويجب أن توفر الـ Csubclass تنفيذ لكل الدوال لمجردة في الواجهة..
ولإنشاء الواجهات Interface بواسطة التجريد بالطبع علينا أن نستخدم وحدة الفئات الأساسية المجردة (abstract base classes) (باختصار وحدة ABC) و @abstractmethod. المثال التالي يوضح طريقة إنشاء الواجهات Interface في بايثون:
from abc import ABC, abstractmethod
# creating interface
class ourInterface(ABC):
@abstractmethod
def WelcomMessage(self):
pass
@abstractmethod
def NumMuiMltiplier(self):
pass
# class implementing the above interface
class interface1(ourInterface):
def WelcomMessage(self):
print ("Welcome to Interface 1")
return
def NumMuiMltiplier(self,x):
return x *2
# class implementing the above interface
class interface2(ourInterface):
def WelcomMessage(self):
print ("Welcome to Interface 2")
return
def NumMuiMltiplier(self,x):
return x *3
#creating instances
obj1 = interface1()
obj1.WelcomMessage()
print (obj1.NumMuiMltiplier(3))
#creating instances
obj2 = interface2()
obj2.WelcomMessage()
print (obj2.NumMuiMltiplier(3))
يتضح لنا من المثال أعلاه كيف تحقق الواجهات مبدأ تعدد الأشكالPolymorphism من خلال التنفيذ المتعدد، ولكن يتوجب علينا التذكير فهنالك نقاط مهمة يجب مراعاة أثناء إنشاء وتنفيذ الواجهات (Interfaces) في بايثون:
- يجب أن تكون الدوال المحددة داخل الواجهة Interface جميعها مجردة (Abstract Methods).
- لا يُسمح بإنشاء كائن (Object) من الـ Class الذي يمثل الواجهة Interface.
- يجب على الفئات الفرعية subclasses التي تنفذ واجهة معينة أن توفر تنفيذ لجميع الدوال لتلك الواجهة.
- في حال عدم توفر تنفيذ محدد للدالة من الواجهة (Interface) الرئيسة في أحد الفئات الفرعية Subclass يجب أن تعرف الدالة على أنها مجردة بالتالي تكون الفئة الفرعية مجردة ايضاً.
يُعتبر تعدد الأشكال (Polymorphism) جزءًا أساسيًا من البرمجة الموجهة للكائنات (OOP) في بايثون، حيث يوفر وسيلة لتبسيط التفاعل مع الكائنات المختلفة باستخدام نفس الواجهات. مما يعزز مرونة الكود وقابليته للتوسع، فيسهل عمليات التطوير والصيانة. ومع استخدام تعدد الأشكال Polymorphism، يمكن للمبرمجين الاستفادة من قوة البرمجة الشيئية لكتابة برامج أكثر تنظيماً.
جميع الأمثلة في هذا المقال متوفرة هنا.
في الختام، يوفر تعدد الأشكال (Polymorphism) في Python وسيلة فعالة لتبسيط التعقيد البرمجي وتعزيز قابلية الصيانة. سواء كنت تعمل على مشاريع صغيرة أو أنظمة كبيرة، فإن استغلال تعدد الأشكال Polymorphism يمكن أن يسهم بشكل كبير في تحسين جودة وكفاءة برامجك. بايثون بمرونتها تجعل هذا المبدأ سهل التنفيذ، مما يجعلها خيارًا مثاليًا للمطورين الذين يسعون لكتابة أكواد نظيفة ومنظمة.