البرمجة

وراثة الأصناف في بايثون 3 – بايثون


تُسِّهل البرمجة الكائنية كتابة شيفرات قابلة لإعادة الاستخدام وتجنب التكرار في مشاريع التطوير. إحدى الآليات التي تحقق بها البرمجة الكائنية هذا الهدف هي مفهوم الوراثة (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

اقرأ أيضًا



Source link

مقالات ذات صلة