تُسِّهل البرمجة الكائنية كتابة شيفرات قابلة لإعادة الاستخدام وتجنب التكرار في مشاريع التطوير. إحدى الآليات التي تحقق بها البرمجة الكائنية هذا الهدف هي مفهوم الوراثة (inheritance)، التي بفضلها يمكن لصنفٍ فرعي (subclass) استخدام الشيفرة الخاصة بصنف أساسي (base class، ويطلق عليه «صنف أب» أيضًا) موجود مسبقًا.
سيستعرض هذا الدرس بعض الجوانب الرئيسية لمفهوم الوراثة في بايثون، بما في ذلك كيفية إنشاء الأصناف الأساسية (parent classes) والأصناف الفرعية (child classes)، وكيفية إعادة تعريف (override) التوابع والخاصيات، وكيفية استخدام التابع super()
، وكيفية الاستفادة من الوراثة المتعددة (multiple inheritance).
ما هي الوراثة؟
تقوم الوراثة على استخدام شيفرة صنف معين في صنف آخر أي يرث صنف يراد إنشاؤه شيفرة صنف آخر. يمكن تمثيل مفهوم الوراثة في البرمجة بالوراثة في علم الأحياء تمامًا، فالأبناء يرثون خاصيات معينة من آبائهم. ويمكن لطفل أن يرث طول والده أو لون عينيه بالإضافة إلى خاصيات أخرى جديدة خاصة فيه. كما يتشارك الأطفال نفس اسم العائلة الخاصة بآبائهم.
ترث الأصناف الفرعية (subclasses، تُسمى أيضًا *الأصناف الأبناء [child classes]) التوابع والمتغيرات من *الأصناف الأساسية* (base classes، تسمى أيضًاالأصناف الآباء [parent classes]).
مثلًا، قد يكون لدينا صنف أساسي يسمى Parent
يحتوي متغيرات الأصناف last_name
و height
و eye_color
، والتي سيرثها الصنف الابن Child
.
لمَّا كان الصنف الفرعي Child
يرث الصنف الأساسي Parent
، فبإمكانه إعادة استخدام شيفرة Parent
، مما يسمح للمبرمج بكتابة شيفرة أوجز، وتقليل التكرار.
الأصناف الأساسية
تشكل الأصناف الأساسية أساسًا يمكن أن تستند إليه الأصناف الفرعية المُتفرِّعة منها، إذ تسمح الأصناف الأساسية بإنشاء أصناف فرعية عبر الوراثة دون الحاجة إلى كتابة نفس الشيفرة في كل مرة. يمكن تحويل أي صنف إلى صنف أساسي، إذ يمكن استخدامه لوحده، أو جعله قالبًا (نموذجًا).
لنفترض أّنّ لدينا صنفًا أساسيًا باسم Bank_account
، وصنفين فرعيين مُشتقين منه باسم Personal_account
و Business_account
. ستكون العديد من التوابع مشتركة بين الحسابات الشخصية (Personalaccount) والحسابات التجارية (Businessaccount)، مثل توابع سحب وإيداع الأموال، لذا يمكن أن تنتمي تلك التوابع إلى الصنف الأساسي Bank_account
. سيكون للصنف Business_account
توابع خاصة به، مثل تابع مخصص لعملية جمع سجلات ونماذج الأعمال، بالإضافة إلى متغير employee_identification_number
موروث من الصنف الأب.
وبالمثل، قد يحتوي الصنف Animal
على التابعين eating()
و sleeping()
، وقد يتضمن الصنف الفرعي Snake
تابعين إضافيين باسم hissing()
و slithering()
خاصين به.
دعنا ننشئ صنفًا أساسيًا باسم Fish
لاستخدامه لاحقًا أساسًا لأصناف فرعية تمثل أنواع الأسماك. سيكون لكل واحدة من تلك الأسماك أسماء أولى وأخيرة، بالإضافة إلى خصائص مميزة خاصة بها.
سننشئ ملفًا جديدًا يسمى fish.py
ونبدأ بالباني، والذي سنعرّف داخله متغيري الصنف first_name
و last_name
لكل كائنات الصنف Fish
، أو أصنافه الفرعية.
class Fish: def __init__(self, first_name, last_name="Fish"): self.first_name = first_name self.last_name = last_name
القيمة الافتراضية للمتغير last_name
هي السلسلة النصية "Fish"
، لأننا نعلم أنّ معظم الأسماك سيكون هذا هو اسمها الأخير.
لنُضف بعض التوابع الأخرى:
class Fish: def __init__(self, first_name, last_name="Fish"): self.first_name = first_name self.last_name = last_name def swim(self): print("The fish is swimming.") def swim_backwards(self): print("The fish can swim backwards.")
لقد أضفنا التابعين swim()
و swim_backwards()
إلى الصنف Fish
حتى يتسنى لكل الأصناف الفرعية استخدام هذه التوابع.
ما دام أنّ معظم الأسماك التي ننوي إنشاءها ستكون عظمية (أي أنّ لها هيكلا عظميًا) وليس غضروفية (أي أن لها هيكلًا غضروفيًا)، فيمكننا إضافة بعض الخاصيات الإضافية إلى التابع __init__()
:
class Fish: def __init__(self, first_name, last_name="Fish", skeleton="bone", eyelids=False): self.first_name = first_name self.last_name = last_name self.skeleton = skeleton self.eyelids = eyelids def swim(self): print("The fish is swimming.") def swim_backwards(self): print("The fish can swim backwards.")
لا يختلف بناء الأصناف الأساسية عن بناء أي صنف آخر، إلا أننا نصممها لتستفيد منها الأصناف الفرعية المُعرّفة لاحقًا.
الأصناف الفرعية
الأصناف الفرعية هي أصناف ترث كل شيء من الصنف الأساسي. هذا يعني أنّ الأصناف الفرعية قادرة على الاستفادة من توابع ومتغيرات الصنف الأساسي.
على سبيل المثال، سيتمكن الصنف الفرعي Goldfish
المشتق من الصنف Fish
من استخدام التابع swim()
المُعرّف في Fish
دون الحاجة إلى التصريح عنه.
يمكننا النظر إلى الأصناف الفرعية على أنها أقسام من الصنف الأساسي. فإذا كان لدينا صنف فرعي يسمى Rhombus
(معيّن)، وصنف أساسي يسمى Parallelogram
(متوازي الأضلاع)، يمكننا القول أنّ المعين (Rhombus
) هو متوازي أضلاع (Parallelogram
).
يبدو السطر الأول من الصنف الفرعي مختلفًا قليلًا عن الأصناف غير الفرعية، إذ يجب عليك تمرير الصنف الأساسي إلى الصنف الفرعي كمعامل:
class Trout(Fish):
الصنف Trout
هو صنف فرعي من Fish
. يدلنا على هذا الكلمةُ Fish
المُدرجة بين قوسين.
يمكننا إضافة توابع جديدة إلى الأصناف الفرعية، أو إعادة تعريف التوابع الخاصة بالصنف الأساسي، أو يمكننا ببساطة قبول التوابع الأساسية الافتراضية باستخدام الكلمة المفتاحية pass
، وهو ما سنفعله في المثال التالي:
... class Trout(Fish): pass
يمكننا الآن إنشاء كائن من الصنف Trout
دون الحاجة إلى تعريف أي توابع إضافية.
... class Trout(Fish): pass terry = Trout("Terry") print(terry.first_name + " " + terry.last_name) print(terry.skeleton) print(terry.eyelids) terry.swim() terry.swim_backwards()
لقد أنشأنا كائنًا باسم terry
من الصنف Trout
، والذي سيستخدم جميع توابع الصنف Fish
وإن لم نعرّفها في الصنف الفرعي Trout
. يكفي أن نمرر القيمة "Terry"
إلى المتغير first_name
، أما المتغيرات الأخرى فقد جرى تهيئتها سلفًا.
عند تنفيذ البرنامج، سنحصل على المخرجات التالية:
Terry Fish bone False The fish is swimming. The fish can swim backwards.
لننشئ الآن صنفًا فرعيًا آخر يعرّف تابعًا خاصا به. سنسمي هذا الصنف Clownfish
. سيسمح التابع الخاص به بالتعايش مع شقائق النعمان البحري:
... class Clownfish(Fish): def live_with_anemone(self): print("The clownfish is coexisting with sea anemone.")
دعنا ننشئ الآن كائنًا آخر من الصنف Clownfish
:
... casey = Clownfish("Casey") print(casey.first_name + " " + casey.last_name) casey.swim() casey.live_with_anemone()
عند تنفيذ البرنامج، سنحصل على المخرجات التالية:
Casey Fish The fish is swimming. The clownfish is coexisting with sea anemone.
تُظهر المخرجات أنّ الكائن casey
المستنسخ من الصنف Clownfish
قادر على استخدام التابعين __init__()
و swim()
الخاصين بالصنف Fish
، إضافة إلى التابع live_with_anemone()
الخاص بالصنف الفرعي.
إذا حاولنا استخدام التابع live_with_anemone()
في الكائن Trout
، فسوف يُطلق خطأ:
terry.live_with_anemone() AttributeError: 'Trout' object has no attribute 'live_with_anemone'
ذلك أنَّ التابع live_with_anemone()
ينتمي إلى الصنف الفرعي Clownfish
فقط، وليس إلى الصنف الأساسي Fish
.
ترث الأصناف الفرعية توابع الصنف الأساسي الذي اشتُقَّت منه، لذا يمكن لكل الأصناف الفرعية استخدام تلك التوابع.
إعادة تعريف توابع الصنف الأساسي
في المثال السابق عرّفنا الصنف الفرعي Trout
الذي استخدم الكلمة المفتاحية pass
ليرث جميع سلوكيات الصنف الأساسي Fish
، وعرّفنا كذلك صنفًا آخر Clownfish
يرث جميع سلوكيات الصنف الأساسي، ويُنشئ أيضًا تابعًا خاصًا به. قد نرغب في بعض الأحيان في استخدام بعض سلوكيات الصنف الأساسي، ولكن ليس كلها. يُطلَق على عملية تغيير توابع الصنف الأساسي «إعادة التعريف» (Overriding).
عند إنشاء الأصناف الأساسية أو الفرعية، فلا بد أن تكون لك رؤية عامة لتصميم البرنامج حتى لا تعيد تعريف التوابع إلا عند الضرورة.
سننشئ صنفًا فرعيًا Shark
مشتقًا من الصنف الأساسي Fish
، الذي سيمثل الأسماك العظمية بشكل أساسي، لذا يتعين علينا إجراء تعديلات على الصنف Shark
المخصص في الأصل للأسماك الغضروفية. من منظور تصميم البرامج، إذا كانت لدينا أكثر من سمكة غير عظمية واحدة، فيُستحب أن ننشئ صنفًا خاصًا بكل نوع من هذين النوعين من الأسماك.
تمتلك أسماك القرش، على عكس الأسماك العظمية، هياكل مصنوعة من الغضاريف بدلاً من العظام. كما أنّ لديها جفونًا، ولا تستطيع السباحة إلى الوراء، كما أنها قادرة على المناورة للخلف عن طريق الغوص.
على ضوء هذه المعلومات، سنعيد تعريف الباني __init__()
والتابع swim_backwards()
. لا نحتاج إلى تعديل التابع swim()
لأنّ أسماك القرش يمكنها السباحة. دعنا نلقي نظرة على هذا الصنف الفرعي:
... class Shark(Fish): def __init__(self, first_name, last_name="Shark", skeleton="cartilage", eyelids=True): self.first_name = first_name self.last_name = last_name self.skeleton = skeleton self.eyelids = eyelids def swim_backwards(self): print("The shark cannot swim backwards, but can sink backwards.")
لقد أعدنا تعريف المعاملات التي تمت تهيئتها في التابع __init__()
، فأخذ المتغير last_name
القيمة "Shark"
، كما أُسنِد إلى المتغير skeleton
القيمة "cartilage"
، فيما أُسنِدَت القيمة المنطقية True
إلى المتغير eyelids
. يمكن لجميع نُسخ الصنف إعادة تعريف هذه المعاملات.
يطبع التابع swim_backwards()
سلسلة نصية مختلفة عن تلك التي يطبعها في الصنف الأساسي Fish
، لأنّ أسماك القرش غير قادرة على السباحة للخلف كما تفعل الأسماك العظمية.
يمكننا الآن إنشاء نسخة من الصنف الفرعي Shark
، والذي سيستخدم التابع swim()
الخاص بالصنف الأساسي Fish
:
... sammy = Shark("Sammy") print(sammy.first_name + " " + sammy.last_name) sammy.swim() sammy.swim_backwards() print(sammy.eyelids) print(sammy.skeleton)
عند تنفيذ هذه الشيفرة، سنحصل على المخرجات التالية:
Sammy Shark The fish is swimming. The shark cannot swim backwards, but can sink backwards. True cartilage
لقد أعاد الصنف الفرعي Shark
تعريف التابعين __init__()
و swim_backwards()
الخاصين بالصنف الأساسي Fish
، وورث في نفس الوقت التابع swim()
الخاص بالصنف الأساسي.
الدالة super()
يمكنك باستخدام الدالة super()
الوصول إلى التوابع الموروثة التي أُعيدت كتابتها.
عندما نستخدم الدالة super()
، فإننا نستدعي التابع الخاص بالصنف الأساسي لاستخدامه في الصنف الفرعي. على سبيل المثال، قد نرغب في إعادة تعريف جانب من التابع الأساسي وإضافة وظائف معينة إليه، ثم بعد ذلك نستدعي التابع الأساسي لإنهاء بقية العمل.
في برنامج خاص بتقييم الطلاب مثلًا، قد نرغب في تعريف صنف فرعي Weighted_grade
يرث الصنف الأساسي Grade
، ونعيد فيه تعريف التابع calculate_grade()
الخاص بالصنف الأساسي من أجل تضمين شيفرة خاصة بحساب التقدير المرجّح (weighted grade)، مع الحفاظ على بقية وظائف الصنف الأساسي. عبر استدعاء التابع super()
، سنكون قادرين على تحقيق ذلك.
عادة ما يُستخدم التابع super()
ضمن التابع __init__()
، لأنّه المكان الذي ستحتاج فيه على الأرجح إلى إضافة بعض الوظائف الخاصة إلى الصنف الفرعي قبل إكمال التهيئة من الصنف الأساسي.
لنضرب مثلًا لتوضيح ذلك، دعنا نعدّل الصنف الفرعي Trout
. نظرًا لأنّ سمك السلمون المرقَّط من أسماك المياه العذبة، فلنضف متغيرًا اسمه water
إلى التابع __init__()
، ولنُعطه القيمة "freshwater"
، ولكن مع الحفاظ على باقي متغيرات ومعاملات الصنف الأساسي:
... class Trout(Fish): def __init__(self, water = "freshwater"): self.water = water super().__init__(self) ...
لقد أعدنا تعريف التابع __init__()
في الصنف الفرعي Trout
، وغيرنا سلوكه موازنةً بالتابع __init__()
المُعرَّف سلفًا في الصنف الأساسي Fish
. لاحظ أننا استدعينا التابع __init__()
الخاص بالصنف Fish
بشكل صريح ضمن التابع __init__()
الخاص بالصنف Trout
،.
بعد إعادة تعريف التابع، لم نعد بحاجة إلى تمرير first_name
كمعامل إلى Trout
، وفي حال فعلنا ذلك، فسيؤدي ذلك إلى إعادة تعيين freshwater
بدلاً من ذلك. سنُهيِّئ بعد ذلك الخاصية first_name
عن طريق استدعاء المتغير في الكائن خاصتنا.
الآن يمكننا استدعاء متغيرات الصنف الأساسي التي تمت تهيئتها، وكذلك استخدام المتغير الخاص بالصنف الفرعي:
... terry = Trout() # تهيئة الاسم الأول terry.first_name = "Terry" # super() الخاص بالصنف الأساسي عبر __init__() استخدام print(terry.first_name + " " + terry.last_name) print(terry.eyelids) # المعاد تعريفها في الصنف الفرعي __init__() استخدام print(terry.water) # الخاص بالصنف الأساسي swim() استخدام التابع terry.swim()
سنحصل على المخرجات التالية:
Terry Fish False freshwater The fish is swimming.
تُظهر المخرجات أنّ الكائن terry
المنسوخ من الصنف الفرعي Trout
قادر على استخدام المتغير water
الخاص بتابع الصنف الفرعي __init__()
، إضافة إلى استدعاء المتغيرات first_name
و last_name
و eyelids
الخاصة بالتابع __init__()
المُعرَّف في الصنف الأساسي Fish
.
يسمح لنا التابع super()
المُضمن في بايثون باستخدام توابع الصنف الأساسي حتى بعد إعادة تعريف تلك التوابع في الأصناف الفرعية.
الوراثة المُتعدِّدة (Multiple Inheritance)
المقصود بالوراثة المتعددة هي قدرة الصنف على أن يرث الخاصيات والتوابع من أكثر من صنف أساسي واحد. هذا من شأنه تقليل التكرار في البرامج، ولكنه يمكن أيضًا أن يُعقِّد العمل، لذلك يجب استخدام هذا المفهوم بحذر.
لإظهار كيفية عمل الوراثة المتعددة، دعنا ننشئ صنفًا فرعيًا Coral_reef
يرث من الصنفين Coral
و Sea_anemone
. يمكننا إنشاء تابع في كل صنف أساسي، ثم استخدام الكلمة المفتاحية pass
في الصنف الفرعي Coral_reef
:
class Coral: def community(self): print("Coral lives in a community.") class Anemone: def protect_clownfish(self): print("The anemone is protecting the clownfish.") class CoralReef(Coral, Anemone): pass
يحتوي الصنف Coral
على تابع يسمى community()
، والذي يطبع سطرًا واحدًا، بينما يحتوي الصنف Anemone
على تابع يسمى protect_clownfish()
، والذي يطبع سطرًا آخر. سنُمرِّر الصنفين كلاهما بين قوسين في تعريف الصنف CoralReef
، ما يعني أنه سيرث الصنفين معًا.
دعنا الآن ننشئ كائنًا من الصنف CoralReef
:
... great_barrier = CoralReef() great_barrier.community() great_barrier.protect_clownfish()
الكائن great_barrier
مُشتقٌ الصنف CoralReef
، ويمكنه استخدام التوابع من كلا الصنفين الأساسيين. عند تنفيذ البرنامج، سنحصل على المخرجات التالية:
Coral lives in a community. The anemone is protecting the clownfish.
تُظهِر المخرجات أنَّ التوابع من كلا الصنفين الأساسيين استُخدِما بفعالية في الصنف الفرعي.
تسمح لنا الوراثة المُتعدِّدة بإعادة استخدام الشيفرات البرمجية المكتوبة في أكثر من صنف أساسي واحد. وإذا تم تعريف التابع نفسه في أكثر من صنف أساسي واحد، فإنّ الصنف الفرعي سيستخدم التابع الخاص بالصنف الأساسي الذي ظهر أولًا في قائمة الأصناف المُمرَّرة إليه عند تعريفه.
رغم فوائدها الكثيرة وفعاليتها، إلا أنَّ عليك توخي الحذر في استخدام الوراثة المُتعدِّدة، حتى لا ينتهي بك الأمر بكتابة برامج مُعقَّدة وغير مفهومة للمبرمجين الآخرين.
خلاصة
تعلمنا في هذا الدرس كيفية إنشاء أصناف أساسية وفرعية، وكيفية إعادة تعريف توابع وخاصيات الأصناف الأساسية داخل الأصناف الفرعية باستخدام التابع super()
، إضافة إلى مفهوم الوراثة المتعددة.
الوراثة هي إحدى أهم ميزات البرمجة الكائنية التي تجعلها متوافقة مع مبدأ DRY (لا تكرر نفسك)، وهذا يحسن إنتاجية المبرمجين، ويساعدهم على تصميم برامج فعالة وواضحة.
هذه المقالة جزء من سلسة مقالات حول تعلم البرمجة في بايثون 3.
ترجمة -وبتصرّف- للمقال Understanding Class Inheritance in Python 3 لصاحبته Lisa Tagliaferri
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.