اذهب إلى المحتوى

التعامل مع البرامج كائنية التوجه والوراثة المتعددة في لغة بايثون


Naser Dakhel

استعرضنا في المقال السابق مفهوم الوراثة في البرامج كائنية التوجه، سنتابع في هذا المقال الموضوع ذاته إذ سنستعرض بعض التوابع المهمة بهذا الخصوص، إضافةً إلى مناقشة مفهوم الوراثة المتعددة الموجودة في لغة بايثون.

الدالتين isinstance()‎ و isssubclass()‎

يمكننا تمرير الكائن عندما نريد معرفة نوعه إلى الدالة type()‎ المضمنة كما تحدثنا سابقًا، ولكن إذا أردنا التحقق من نوع كائن فيُفضل استخدام الدالة المبنية مسبقًا isinstance()‎، التي تعيد الدالة قيمة True إذا كان الكائن في الصنف المعطى أو صنفها الفرعي.

اكتب ما يلي في الصدفة التفاعلية:

>>> class ParentClass:
...     pass
...
>>> class ChildClass(ParentClass):
...     pass
...
>>> parent = ParentClass() # إنشاء كائن ‫ParentClass
>>> child = ChildClass() # إنشاء كائن ‫ChildClass
>>> isinstance(parent, ParentClass)
True
>>> isinstance(parent, ChildClass)
False
1 >>> isinstance(child, ChildClass)
True
2 >>> isinstance(child, ParentClass)
True

لاحظ أن isinstance()‎ تشير إلى أن كائن ChildClass في child هو نسخةٌ من ‎ChildClass‎ (السطر ذو الرقم 1) ونسخةٌ من ‎ParentClass‎ (السطر ذو الرقم 2)، وهذا منطقي لأن كائن ChildClass له علاقة من نوع "is a" مع نوع كائن ParentClass، أي أنه نوع من هذا الكائن.

يمكن أيضًا تمرير صف tuple من كائنات الأصناف مثل وسيط ثانٍ لمعرفة إذا كان الوسيط الأول هو واحد من الأصناف الموجودة في الصف:

# ‫تُعيد True إذا كانت القيمة 42 عددًا صحيحًا أو سلسلة نصية أو قيمة بوليانية
>>> isinstance(42, (int, str, bool)) 
True if 42 is an int, str, or bool.
True

الدالة المضمنة الأخرى issubclass()‎ أقل شيوعًا من isinstance()‎ ويمكنها التعرُّف ما إذا كان كائن الصنف الممر إلى الوسيط الأول هو صنف فرعي (أو نفس الصنف) لكائن الصنف المرر إلى الوسيط الثاني:

>>> issubclass(ChildClass, ParentClass) # ‫ChildClass صنف فرعي من ParentClass
True
>>> issubclass(ChildClass, str) # ‫ChildClass ليس صنفًا فرعيًا من من str
False
>>> issubclass(ChildClass, ChildClass) # ChildClass هو ChildClass
True

يمكنك تمرير صف من كائنات الصنف بمثابة وسيط ثاني إلى issubclass()‎ كما هو الحال مع Isinstance()‎، وذلك لرؤية ما إذا كان الوسيط الأول هو صنف فرعي لأي من الأصناف في الصف. الفارق الأساسي بين isinstance()‎ و issubclass()‎ هو أن issubclass()‎ تمرر كائني صنف و isinstance()‎ تمرر كائن وكائن صنف.

توابع الصنف

ترتبط توابع الصنف مع صنف أكثر مقارنةً بالكائنات المفردة مثل التوابع العادية. يمكنك ملاحظة تابع الصنف في الشيفرة عندما ترى علامتين، هما: المزخرف ‎@‎classmethod قبل تعليمة التابع def، واستخدام cls معاملًا أولًا كما في المثال التالي:

class ExampleClass:
    def exampleRegularMethod(self):
        print('This is a regular method.')

    @classmethod
    def exampleClassMethod(cls):
        print('This is a class method.')

# استدعاء تابع الصنف دون إنشاء نسخة كائن 
ExampleClass.exampleClassMethod()

obj = ExampleClass()
# بالنظر إلى السطر السابق، السطرين التاليين متكافئين
obj.exampleClassMethod()
obj.__class__.exampleClassMethod()

يعمل المعامل cls مثل self ولكن self تشير إلى كائن بينما يشير المعامل cls إلى صنف الكائن، هذا يعني أن الشيفرة في تابع الصنف لا يمكنها الوصول إلى خاصيات الكائن المفردة أو استدعاء توابع الكائن العادية. تستدعي توابع الأصناف توابع أصناف أخرى وتستطيع الوصول إلى سمات الصنف. نستخدم الاسم cls لأن class هي كلمة مفتاحية في بايثون وكما هو الحال مع باقي الكلمات المفتاحية مثل if و while و import، فنحن لا نستطيع استخدامها في أسماء المعاملات، ونستدعي غالبًا سمات الأصناف من خلال كائن الصنف، مثل ExampleClass.exampleClassMethod()‎، إلا أنه يمكننا استدعاؤهم من خلال أي كائن من الصنف كما في obj.exampleClassMethod()‎.

لا تُستخدم توابع الصنف عمومًا وأكثر الحالات استخدامًا هي لتوفير بديل عن توابع الباني constructorإضافةً للتابع ‎__‎‎init‎__()‎. على سبيل المثال، ماذا لو كانت دالة الباني تقبل سلسةً نصيةً من البيانات يحتاجها الكائن الجديد أو سلسلة نصية لاسم ملف يحتوي البيانات التي يحتاجها الكائن الجديد؟ لا نحتاج إلى قائمة معاملات التابع ‏‏()‏‏__init__ لأنها ستكون طويلة ومعقدة، ونستخدم تابع دالة يعيد كائن جديد بدلًا من ذلك.

مثلًا، لننشئ صنف AsciiArt (مررنا عليه سابقًا) الذي يستخدم محارف نصية ليشكل صورة:

class AsciiArt:
    def __init__(self, characters):
        self._characters = characters

    @classmethod
    def fromFile(cls, filename):
        with open(filename) as fileObj:
            characters = fileObj.read()
            return cls(characters)

    def display(self):
        print(self._characters)

    # Other AsciiArt methods would go here...

face1 = AsciiArt(' _______\n' +
                 '|  . .  |\n' +
                 '| \\___/ |\n' +
                 '|_______|')
face1.display()

face2 = AsciiArt.fromFile('face.txt')
face2.display()

لدى صنف AsciiArt تابع ()__init__ الذي يمكن أن يمرر محارف النص الصورة مثل سلسلة نصية. لديه أيضًا تابع صنف fromFile()‎ الذي يمكن أن يمرر السلسلة النصية لاسم الملف مثل ملف نصي يحتوي فن آسكي ASCII art. يُنشئ كلا التابعين كائنات AsciiArt.

نفذ البرنامج وسيكون هناك ملف face.txt يحتوي على وجه فن آسكي ASCII، ليكون الخرج على النحو التالي:

_______
|  . .  |
| \___/ |
|_______|
 _______
|  . .  |
| \___/ |
|_______|

يجعل تابع الصنف fromFile()‎ الشيفرة الخاصة بك سهلة القراءة مقارنةً بجعل ()__init__ يفعل كل شيء.

ميزة أُخرى لتابع الصنف هو أن صنف فرعي من AsciiArt يمكن أن يرث تابع fromFile()‎ الخاص (وإعادة تعريفه إذا لزم)، وهذا هو سبب استدعاء cls(characters)‎ في تابع صنف AsciiArt بدلًا من AsciiArt(characters)‎. يعمل استدعاء ()cls أيضًا في الأصناف الفرعية للصنف AsciiArt دون تعديل لأن صنف AsciiArt ليس متوفرًا في التابع، ولكن استدعاء AsciiArt()‎ يستدعي ()__init__ الخاص بصنف AsciiArt بدلًا من ()__init__ الخاص بالصنف الفرعي. يمكنك التفكير في cls على أنها "كائن يمثل هذا الصنف".

خذ بالحسبان أنه يجب أن تستخدم التوابع العادية معامل self في مكان ما في الشيفرة الخاصة بهم، ويجب على تابع الصنف دائمًا استخدام المعامل cls. إذا لم يستخدم أبدًا تابع الصنف المعامل cls، فهذه إشارة أن تابع الصنف الخاص بك يجب أن يكون تابعًا عاديًا.

سمات الأصناف

سمة الصنف هي متغير ينتمي إلى صنف بدلًا من كائن. ننشئ سمة صنف داخل الصنف ولكن خارج كل التوابع كما أنشأنا متغيرات عامة في ملف "‎.py" ولكن خارج كل الدوال. هذا مثال عن سمة صنف اسمها count التي تحصي عدد كائنات CreatCounter المُنشأة.

class CreateCounter:
    count = 0 # هذه سمة لصنف

    def __init__(self):
        CreateCounter.count += 1

print('Objects created:', CreateCounter.count)  # تطبع 0
a = CreateCounter()
b = CreateCounter()
c = CreateCounter()
print('Objects created:', CreateCounter.count)  # تطبع 3

لدى صنف CreatCounter سمة صنف واحدة اسمها count. كل كائنات CreatCounter لديهم هذه السمة بدلًا من أن يكون لكل منهم سمات count منفصلة. لهذا يعد السطر CreateCounter.count += 1 في دالة الباني كل كائن CreatCounter مُنشأ.

عندما تنفذ البرنامج، يكون الخرج على النحو التالي.

Objects created: 0
Objects created: 3

نادرًا ما نستخدم سمات الصنف حتى هذا المثال "عد كم كائن CreatCounter مُنشأ" يمكن عمله باستخدام متغير عام بدلًا من سمة صنف.

التوابع الساكنة

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

نعرّف التوابع الساكنة بوضع مزخرف ‎@‎staticmethod قبل تعليمة def الخاصة بهم. هذا مثال عن تابع ساكن:

class ExampleClassWithStaticMethod:
    @staticmethod
    def sayHello():
        print('Hello!')

# لم يُنشأ أي كائن، فاسم الصنف يسبق‪ ‪ sayHello()
Note that no object is created, the class name precedes sayHello():
ExampleClassWithStaticMethod.sayHello()

لا يوجد فرق تقريبًا بين التابع الساكن ‏‏sayHello‎()‎‏‏‏ في صنف ExampleClassWithStaticMethod والدالة sayHello()‎. ربما تفضل بالواقع استخدام دالة لأنك تستطيع استدعائها دون الدخول إلى اسم الصنف مسبقًا.

التوابع الساكنة شائعة في لغات برمجة أخرى ليس لديها ميزات لغة بايثون المرنة. تضمين التوابع الساكنة inclusion of static methods في بايثون هو لمحاكاة اللغات الأخرى ولا يقدم قيمةً عملية.

متى تستخدم الأصناف والميزات كائنية التوجه الساكنة؟

نادرًا ما تحتاج لاستخدام توابع الصنف وسمات الصنف والتوابع الساكنة، فهم عرضةً للاستخدام الزائد إذا كنت تعتقد بالتساؤل التالي: " لماذا لا استخدم الدوال أو المتغيرات العامة بدلًا عن ذلك؟" هذا تلميح لعدم استخدام توابع الأصناف أو سمات الأصناف أو التوابع الساكنة.

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

للمزيد عن هذه الميزات وعن احتياجهم أو لا، اقرأ منشور فيليب ج. ايبي Phillip J. Eby "بايثون ليس جافا" الموجود على الرابط dirtsimple.org/2004/12/python-is-not-java.html ومنشور ريان تومايكو Ryan Tomayko "مفهوم التابع الساكن" على الرابط tomayko.com/blog/2004/the-static-method-thing.

كلمات مهمة كائنية التوجه

يبدأ شرح البرمجة كائنية التوجه OOP بالكثير من المصطلحات مثل الوراثة والتغليف Encapsulation والتعددية الشكلية Polymorphism. أهمية معرفة هذه المصطلحات مبالغ فيه، ولكن يجب عليك أن يكون لديك فهم أساسي لهم، شرحنا الوراثة سابقًا، لذا سنشرح المصطلحات الباقية تاليًا، ويمكنك الاطلاع على مقال البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C على أكاديمية حسوب لمزيدٍ من المعلومات حول هذه المصطلحات.

التغليف

لدى كلمة تغليف معنيين شائعين ولكن متقاربين. التعريف الأول هو تجميع البيانات المتعلقة والشيفرة في وحدة واحدة، أي لتغلف يعني أن تضع في صندوق. هذا ما تفعله الأصناف عمومًا؛ فهي تدمج السمات والتوابع. مثلًا، يغلف صنف WizCoin ثلاثة أعداد صحيحة لـ knuts و sickles و galleons إلى كائن WizCoin واحد.

أما التعريف الثاني فهو تقنية لإخفاء المعلومات تسمح للكائنات بإخفاء تفاصيل تنفيذ معقدة عن كيفية عمل الكائنات. رأينا ذلك في "السمات والتوابع الخاصة"، إذ يقدم كائن BankAccount توابع deposit()‎ و withdraw()‎ لإخفاء تفاصيل كيفية التعامل مع السمة ‎_‎balance. تعمل الدوال مثل صندوق أسود: كيفية حساب الدالة math.sqrt()‎ للجذر التربيعي لأي رقم مخفية، كل ما عليك معرفته هو أن تعيد الدالة الجذر التربيعي للرقم الممرر لها.

التعددية الشكلية polymorphism

تسمح التعددية الشكلية بمعالجة كائنات من نوع ما على أنها كائنات من نوع آخر، فمثلًا تعيد الدالة ()len طول الوسيط الممرر إليها، ويمكنك تمرير سلسلة نصية إلى هذه الدالة لمعرفة عدد المحارف المكونة منه، وكذلك يمكن قائمة list أو قاموس dictionary لمعرفة عدد العناصر، أو عدد أزواج مفتاح-قيمة key-value على الترتيب. يدعى نموذج التعددية الشكلية هذا باسم الدوال المعممة generic functions أو التعددية الشكلية القياسية parametric polymorphism لأنها تعالج كائنات ذات أنواع مختلفة.

يمكنك الاطلاع على مقال مفهوم البرمجة المعممة Generic Programming على أكاديمية حسوب لمزيدٍ من المعلومات عن البرمجة المعممة. يمكن الإشارة إلى التعددية الشكلية بمصطلح التعددية الشكلية الخاصة ad hoc polymorphism أو زيادة تحميل للعامل operator overloading، إذ يمكن أن يأخذ المعامل (مثل + أو *) سلوكًا مختلفًا اعتمادًا على نوع الكائنات التي تُجرى عليها العملية؛ فمثلًا يجري المعامل + عملية الجمع الحسابي عندما تُجرى العملية على عددين صحيحين أو عشريين، لكنها توصل السلاسل النصية في حال كانت العملية على سلسلتين بدلًا من عددين.

لماذا لا نستخدم الوراثة؟

من السهل زيادة تعقيد الأصناف باستخدام الوراثة. كما يشير لوتشيانو رامالهو Luciano Ramalho : "وضع الكائنات في هرمية يرضي حس الترتيب لدينا، ولكن المبرمجين يفعلونها للتسلية". ننشئ أصناف وأصناف فرعية وأصناف تحت فرعية عندما تكون الحاجة لصنف واحد أو اثنين في الوحدة لتفي بالغرض، ولكن تذكر حكمة بايثون الأهم التي ناقشناها سابقًا؛ الحل البسيط أفضل من الحل المعقد.

يسمح استخدام البرمجة كائنية التوجه OOP بتنظيم الشيفرة الخاصة بك إلى وحدات units (في هذه الحالة أصناف) سهلة التعامل بدلًا من ملف "‎.py" واحد يحتوي مئات التوابع المعرفة بدون ترتيب معين. تفيد الوراثة إذا كان لديك عدة دوال تعمل في نفس القاموس أو هيكل قائمة البيانات؛ ففي هذه الحالة من المفيد ترتيبهم في صنف.

هناك بعض الأمثلة لعدم إنشاء الأصناف أو استخدام الوراثة:

  • إذا كان الصنف يتألف من توابع لا تستخدم المعاملين self و cls، احذف الأصناف واستخدم الدوال بدلًا من التوابع.
  • إذا أنشأت أب بصنف ابن واحد ولم تُنشئ كائنات من الصنف الأب، يمكنك جمعهم بصنف واحد.
  • إذا أنشأت أكثر من ثلاثة أو أربعة مستويات من الأصناف الفرعية، ربما تكون قد استخدمت الوراثة على نحوٍ زائد، اجمع هذه الأصناف الفرعية إلى أصناف أقل.

كما توضّح سابقًا في نسختي برنامج إكس أو tic-tac-toe (مع برمجة كائنية التوجه وبدونها)، من الممكن الحصول على برنامج يعمل على نحوٍ سليم وبدون أخطاء دون استخدام الأصناف. لست بحاجة لتصميم برنامج مثل شبكة معقدة من الأصناف، إذ أن الحل البسيط أفضل من الحل المعقد الذي لا يعمل. يتحدث جول سبلوسكي Joel Spolsky عن ذلك في منشوره "لا تدع المصممين رواد الفضاء أن يخيفوك" الموجود على الرابط joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you.

يجب أن تعرف الآن كيفية عمل مفاهيم البرمجة كائنية التوجه مثل الوراثة، لأنها تساعدك على تنظيم الشيفرة الخاصة بك وجعل التطوير ومعالجة الأخطاء أسهل. تتمتع لغة بايثون بالمرونة، فهي تقدم لك ميزات برمجة كائنية التوجه، لكنها أبضًا لا تطلب منك استخدامها عندما لا تناسب احتياجات البرنامج الخاص بك.

الوراثة المتعددة

في العديد من لغات البرمجة يكون الصنف أب واحد فقط، ولكن بايثون تدعم آباء متعددين عن طريق تقديم ميزة تدعى الوراثة المتعددة multiple inheritance. مثلًا، يمكننا الحصول على صنف Airplane مع تابع flyInTheAir()‎ وصنف Ship مع تابع floatOnWater()‎، ويمكننا إنشاء صنف FlyingBoat يرث كلًا من Airplane و Ship عن طريق تحديدهما في تعليمة class مفصولين بفواصل.

افتح ملف جديد في محرر النصوص واحفظ التالي flayingboat.py:

class Airplane:
    def flyInTheAir(self):
        print('Flying...')

class Ship:
    def floatOnWater(self):
        print('Floating...')

class FlyingBoat(Airplane, Ship):
    pass

سيرث الكائن المُنشأ التابعين flyInTheAir()‎ و floatOnWater()‎ كما سنرى في الصدفة التفاعلية:

>>> from flyingboat import *
>>> seaDuck = FlyingBoat()
>>> seaDuck.flyInTheAir()
Flying...
>>> seaDuck.floatOnWater()
Floating...

الوراثة المتعددة مفهوم بسيط طالما كانت أسماء توابع الأصناف مميزة ولا تتقاطع، وتسمى هذه الأصناف mixins (هذا مصطلح عام لهذا النوع من الأصناف، إذ لا يوجد في بايثون كلمة mixin مفتاحية)، ولكن ماذا سيحصل إذا ورثنا عدة أصناف معقدة تتشارك بأسماء التوابع؟

مثلًا تذكر أصناف لوحة إكس أو ‏‎MiniBoard‏ و HintTTTBoard سابقًا، ماذا لو أردنا صنف يظهر لوحة إكس أو مصغرة مع تقديم بعض النصائح؟ يمكننا إعادة استخدام هذه الأصناف الموجودة باستخدام الوراثة المتعددة. ضِف التالي إلى نهاية ملف tictactoe_oop.py ولكن قبل تعليمة if التي تستدعي الدالة main‎()‎:

class HybridBoard(HintBoard, MiniBoard):
    pass

لا يوجد شيء في هذا الصنف، إذ يُعيد استخدام الشيفرة عن طريق وراثة HintBoard و MiniBoard. عدّل الشيفرة في الدالة main()‎ لتُنشئ كائن HybridBoard:

gameBoard = HybridBoard() # إنشاء كائن‫ TTT للّوحة

لدى كلا الصنفين الأب MiniBoard و HintBoard تابع اسمه getBoardStr()‎ فما الذي ترثه HybridBoard؟ عندما تنفذ البرنامج سيظهر الخرج لوحة إكس أو مصغرة تحتوي على بعض التلميحات:

--snip--
          X.. 123
          .O. 456
          X.. 789
X can win in one more move.

يبدو أن بايثون دمجت سحريًا تابع getBoardStr()‎ الخاص بصنف MiniBoard و getBoardStr()‎ الخاص بصنف HintBoard، وهذا ممكن لأننا كتبنا التابعين بشكل يمكّنهما العمل مع بعضهما. إذا بدلت ترتيب الأصناف في تعليمة class في صنف HybridBoard لتصبح على النحو التالي:

class HybridBoard(MiniBoard, HintBoard): 

فستخسر التلميحات كليًا:

--snip--
          X.. 123
          .O. 456
          X.. 789

لتفهم لماذا حصل ذلك يجب عليك فهم ترتيب استبيان التوابع method resolution order -أو اختصارًا MRO- الخاص ببايثون وكيفية عمل دالة super()‎.

ترتيب استبيان التابع

لدى برنامج إكس أو الخاص بنا أربعة أصناف لتمثيل الألواح، ثلاثة معرفة بتابع getBoardStr()‎ وواحدة بتابع getBoardStr()‎ موروث كما في الشكل 2

الوراثة في بايثون

[الشكل 2: الأصناف الأربعة في برنامج لوحات إكس أو]

عندما نستدعي getBoardStr()‎ على الكائن HybridBoard، يعرف بايثون أن الصنف HybridBoard ليس لديه تابع بذلك الاسم لذا تفحص أصناف الأب، ولكن لدى الصنف هذا صنفين أب وكلاهما لديه تابع getBoardStr()‎، أي منها يُستدعى؟

يمكننا معرفة ذلك من التحقق من ترتيب استبيان التابع MRO الخاص بصنف HybridBoard وهي القائمة المرتبة من الأصناف التي يتحقق منها بايثون عند وراثة التوابع، أو عندما يستدعي التابع دالة super()‎. يمكنك رؤية ترتيب استبيان التابع للصنف HybridBoard عن طريق استدعاء mro()‎ في الصدفة التفاعلية:

>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]

يمكنك من خلال القيمة المُعادة رؤية أنه عندما يُستدعى التابع على HybridBoard، يتحقق بايثون من صنف HybridBoard؛ فإذا لم يكن موجودًا، يتحقق بايثون من صنف HintBoard وبعدها من صنف MiniBoard وأخيرًا من صنف TTTBoard. في آخر كل قائمة ترتيب استبيان الدوال MRO هناك صنف object مضمّن يمثل الصنف الأب لكل الأصناف في بايثون.

معرفة ترتيب استبيان الدوال MRO من أجل وراثة واحدة أمر سهل؛ فقط اصنع سلسلة chain من أصناف الأب، أما بالنسبة للوراثة المتعددة سيكون الأمر أصعب. يتبع ترتيب استبيان الدوال MRO الخاص ببايثون خوارزمية C3 -التي تقع تفاصيل مناقشتها خارج سياق موضوعنا- ولكنك تستطيع تحديد ترتيب استبيان الدوال MRO بتذكر قاعدتين:

  • يتحقق بايثون من الأصناف الابن قبل أصناف الأب.
  • يتحقق من الأصناف الموروثة في القائمة من اليسار إلى اليمين في تعليمة class.

إذا استدعينا getBoardStr()‎ على كائن HybridBoard، يتحقق بايثون من الصنف HybridBoard أولًا وبعدها ونظرًا لكون أصناف الأب من اليسار إلى اليمين هي HintBoard و MiniBorad، يتحقق بايثون من HintBoard. لدى الصنف الأب هذا تابع getBoardStr()‎ لذا يرثها HybridBorad ويستدعيها.

لا ينتهي الأمر هنا، يستدعي التابع super().getBoardStr()‎، إذ أن كلمة "super" هي كلمة مضللة نوعًا ما لدالة super()‎ الخاصة ببايثون، لأنها لا تعيد الصنف الأب ولكن الصنف الذي يليها في ترتيب استبيان التوابع MRO، وهذا يعني عندما نستدعي getBoardStr()‎ على الكائن HybridBoard، يكون الصنف التالي في ترتيب استبيان التوابع MRO بعد HintBoard هو MiniBoard وليس الصنف الأب TTTBoard، لذا استدعاء super().getBoardStr()‎ يستدعي تابع getBoardStr()‎ لصنف MiniBoard الذي يعيد سلسلة نصية للوحة إكس أو المصغرة. تعلّق الشيفرة المتبقية في getBoardStr()‎ الخاصة بصنف HintBoard بعد استدعاء super()‎ نص التلميح لهذه السلسة النصية.

إذا غيرنا تعليمة class في صنف HybridBoard لتضع MiniBoard أولًا و HintBoard ثانيًا، سيضع ترتيب استبيان التوابع MRO الصنف ‏MiniBoard قبل الصنف HintBoard، ما يعني أن HybridBorad ترث getBoardStr()‎ من MiniBoard التي لا تحتوي استدعاء super()‎. هذا الترتيب هو الذي سبّب الخطأ الذي جعل لوحة إكس أو المصغرة تظهر بدون تلميحات؛ فبدون استدعاء super()‎ تابع getBoardStr()‎ الخاص بصنف MiniBoard لا يستدعي تابع getBoardStr()‎ الخاص بصنف HintBoard.

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

الخلاصة

كما تعيد type()‎ نوع الكائن المرر لها، تعيد توابع isinstace()‎ و issubclass()‎ نوع ومعلومات الوراثة عن الكائن الممرر لها.

يمكن أن تحتوي الأصناف توابع كائن وسمات، لكنها تحتوي أيضًا توابع صنف وسمات صنف وتوابع ساكنة، على الرغم من أنها نادرة الاستخدام لكن يمكنها أن تسمح بالتقنيات كائنية التوجه التي لا تستطيع المتغيرات العامة والدوال أن تقدمها.

بسمح بايثون للأصناف أن ترث من عدة آباء، على الرغم من أن ذلك ينتج شيفرة صعبة الفهم. تستطيع دالة super()‎ وتوابع الصنف اكتشاف كيف سترث التوابع اعتمادًا على ترتيب استبيان التوابع MRO، إذ يمكنك مشاهدة ترتيب استبيان التوابع MRO الخاص بصنف في الصدفة التفاعلية عن طريق استدعاء التابع mro()‎ على الصنف.

غطينا في هذا المقال والمقالات السابقة مفاهيمًا عامة في البرمجة كائنية التوجه OOP، وسنتحدث تاليًا عن تقنيات برمجة كائنية التوجه OOP خاصة ببايثون.

ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...