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

البرمجة كائنية التوجه Object-Oriented Programming والوراثة Inheritance في بايثون


Naser Dakhel

يوفر عليك استدعاء وتعريف الدوال من عدة أماكن نسخ ولصق الشيفرة المصدرية، إذ أن عدم تكرار الشيفرة هو ممارسة جيدة لأنه إذا أردت تغيير هذه الشيفرة المكرّرة (إما لحل بعض الأخطاء أو لإضافة ميزات جديدة)، فستحتاج فقط لتغييرها في مكان واحد، ويصبح البرنامج أقصر دون شيفرة مكررة وأسهل للقراءة.

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

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

كيف تعمل الوراثة

نضع اسم الصنف الأب الموجود أساسًا بين قوسين في تعليمة class لإنشاء صنف ابن. لتتدرب على إنشاء صنف ابن، نفتح نافذة محرر الملفات ونكتب الشيفرة التالية ونحفظها في ملف inheritanceExample.py:

1 class ParentClass:
2     def printHello(self):
        print('Hello, world!')

3 class ChildClass(ParentClass):
    def someNewMethod(self):
        print('ParentClass objects don't have this method.')

4 class GrandchildClass(ChildClass):
    def anotherNewMethod(self):
        print('Only GrandchildClass objects have this method.')

print('Create a ParentClass object and call its methods:')
parent = ParentClass()
parent.printHello()

print('Create a ChildClass object and call its methods:')
child = ChildClass()
child.printHello()
child.someNewMethod()

print('Create a GrandchildClass object and call its methods:')
grandchild = GrandchildClass()
grandchild.printHello()
grandchild.someNewMethod()
grandchild.anotherNewMethod()

print('An error:')
parent.someNewMethod()

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

Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
Traceback (most recent call last):
  File "inheritanceExample.py", line 35, in <module>
    parent.someNewMethod() # ParentClass objects don't have this method.
AttributeError: 'ParentClass' object has no attribute 'someNewMethod'

أنشأنا ثلاثة أصناف ‎‎ParentClass‎ في السطر (1) و ‎ChildClass‎ في السطر (3) و ‎GrandchildClass‎ في السطر (4). الصنف ChildClass هو صنف فرعي للصنف ParentClass، يعني أن ChildClass لديها توابع ParentClass نفسها، ونقول أن ChildClass يرث التوابع من ParentClass، وأيضًا GrandchildClass هو صنف فرعي من ChildClass وبالتالي لديه كل توابع ChildClass وأبيها ParentClass.

نسخنا ولصقنا الشيفرة من التابع printHello()‎ باستخدام هذه الطريقة إلى الصنفين ChildClass و GrandChild. أي تغيير للشيفرة في PrintHello()‎ لا يحدث فقط في ParentClass بل في ChildClass و GrandchildClass. هذا نفس تغيير الشيفرة في دالة تُحدّث كل استدعاءات الدالة الخاصة بها. يمكنك رؤية هذه العلاقة في الشكل 1. لاحظ في مخططات الأصناف أن السهم ينطلق من الصنف الفرعي ويشير إلى الصنف الأساس. هذا يعكس أن كل صنف يعرف دائمًا الصنف الأساس الخاص به ولكنه لا يعرف أصنافه الفرعية.

البرمجة كائنية التوجه Object-Oriented Programming في بايثون

[الشكل 1: مخطط هرمي (يسار) ومخطط فين Venn (يمين) يبينان العلاقات بين الأصناف الثلاثة والتوابع التي يستخدموها]

تمثّل الأصناف أب- ابن عادةً علاقات "is a"، إذ أن كائن ChildClass هو كائن ParentClass لأن لديه نفس التوابع التي لدى كائن ParentClass، إضافةً إلى بعض التوابع الإضافية التي يعرّفها. هذه هي علاقة باتجاه واحد: ليس كائن ParentClass هو كائن ChildClass. إذا حاول كائن استدعاء someNewMethod()‎ الموجود فقط لكائنات ChildClass (وأصناف ChildClass الفرعية) يعطي بايثون Python خطأ AttributeError.

يمكنك الاطلاع على مقال مخططات الفئات (Class Diagram) في لغة النمذجة الموحدة UML على أكاديمية حسوب لمزيدٍ من المعلومات على علاقات "is a" وغيرها في مخططات الأصناف.

يعتقد المبرمجون أن الأصناف المتعلقة ببعضها تندرج تحت هرمية علاقات "is a" واقعية، فغالبًا ما ترى في تدريبات البرمجة كائنية التوجه OOP أصناف أب وابن وحفيد:

Vehicle▶FourWheelVehicle▶Car

أو

Animal▶Bird▶Sparrow

أو

Shape▶Rectangle▶Square

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

نسمي أحيانًا الصنف الابن بالصنف الفرعي subclass أو الصنف المُشتق derived class ونسمي الصنف الأب الصنف الأعلى super class أو الصنف الأساس base class، ويمكنك الاطلاع على مقال الوراثة والتعددية الشكلية Polymorphism والأصناف المجردة Abstract Classes في جافا على أكاديمية حسوب لمزيدٍ من المعلومات.

إعادة تعريف التوابع

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

لتوضيح هذا المفهوم لنعد إلى لعبة إكس أو Tic-Tac-Toe التي أنشأناها سابقًا، ولكن هذه المرة سننشئ صنفًا جديدًا MiniBoard وهو صنف فرعي من TTTBoard يعيد تعريف getBoardStr()‎ لرسم لوحة إكس أو أصغر. سيسأل البرنامج أي نوع لوح سيستخدم ولا نحتاج إلى نسخ ولصق باقي توابع TTTBoard لأن MiniBoard سيرثهم.

ضِف التالي في نهاية ملف ticktactoe_oop.py لإنشاء صنف ابن لصنف TTTBoard الأصلي، ثم أعد كتابة تابع getBoardStr()‎:

class MiniBoard(TTTBoard):
    def getBoardStr(self):
        """Return a tiny text-representation of the board."""
        # Change blank spaces to a '.'
        for space in ALL_SPACES:
            if self._spaces[space] == BLANK:
                self._spaces[space] = '.'

        boardStr = f'''
          {self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123
          {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456
          {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789'''

        # Change '.' back to blank spaces.
        for space in ALL_SPACES:
            if self._spaces[space] == '.':
                self._spaces[space] = BLANK
        return boardStr

كما في التابع ()getBoardStr الخاص بصنف TTTBoard سينشئ التابع getBoardStr()‎ الخاص بـ MiniBoard لإظهار سلسلة نصية متعددة الأسطر من لوحة إكس أو عندما تمرر إلى دالة print()‎ ولكن هذه السلسلة النصية هي أقصر وتتجاهل الأسطر بين X و O وتستخدم الفواصل للدلالة على الأماكن الفارغة.

غيّر السطر في main()‎ ليستنسخ كائن MiniBoard بدلًا من كائن TTTBoard:

  if input('Use mini board? Y/N: ').lower().startswith('y'):
        gameBoard = MiniBoard() # Create a MiniBoard object.
    else:
        gameBoard = TTTBoard() # Create a TTTBoard object.

يعمل البرنامج كما في السابق ما عدا تغيير هذا السطر الواحد في main()‎ وعندما تنفذ البرنامج الآن سيصبح الخرج على النحو التالي:

Welcome to Tic-Tac-Toe!
Use mini board? Y/N: y

          ... 123
          ... 456
          ... 789
What is X's move? (1-9)
1

          X.. 123
          ... 456
          ... 789
What is O's move? (1-9)
--snip--
          XXX 123
          .OO 456
          O.X 789
X has won the game!
Thanks for playing!

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

يمكننا إضافة سمة attribute جديدة إلى TTTBoard اسمها useMiniBoard إذا لم نستخدم الوراثة، ووضع تعليمة if-else داخل getBoardStr()‎ لتقرر متى تُظهِر اللوحة العادية أو اللوحة المصغرة، سيعمل هذا جيدًا لأن التغيير بسيط، ولكن ماذا لو كان الصنف الفرعي MiniBoard يحتاج لإعادة تعريف تابعين أو ثلاثة توابع أو حتى 100 تابع؟ ماذا لو أردنا إنشاء عدة أصناف فرعية من TTTBoard؟ سيتسبّب عدم استخدام الوراثة بسيل من تعليمات if-else داخل التابع الخاص بنا وزيادة كبيرة في تعقيد الشيفرة. يُمكّننا استخدام الأصناف الفرعية وإعادة تعريف التوابع من ترتيب الشيفرة الخاصة بنا ضمن أصناف منفصلة للتعامل مع حالات استخدام مماثلة.

دالة super()‎

يشابه تابع الصنف المعاد تعريفه overridden تابع الصنف الأب؛ فحتى لو كانت الوراثة هي تقنية لإعادة استخدام الشيفرة، قد يتطلب إعادة تعريف التابع إعادة كتابة نفس الشيفرة من تابع الصنف الأب بمثابة جزء من تابع شيفرة الابن. لمنع تكرار الشيفرة: تسمح دالة super()‎ للتابع المعاد تعريفه استدعاء التابع الأصلي في الصنف الأب.

مثلًا، لنُنشئ صنفًا جديدًا اسمه HintBoard ليكون صنفًا فرعيًا من TTTBoard، بحيث يعيد هذا الصنف تعريف getBoardStr()‎، ويضيف بعد رسم لوحة إكس أو تلميحًا hint فيما إذا كان X أو O قد يربح في الخطوة التالية. هذا يعني أن تابع getBoardStr()‎ الخاص بصنف HintBoard سينجز نفس مهام تابع getBoardStr()‎ الخاص بصنف TTTBoard لرسم لوحة إكس أو. بدلًا من تكرار الشيفرة لإنجاز ذلك، يمكننا استخدام super()‎ لاستدعاء تابع getBoardStr الخاص بصنف TTTBoard من تابع getBoardStr()‎ الخاص بصنف HintBoard. ضِف التالي لنهاية ملف tictactoe_oop.ps:

class HintBoard(TTTBoard):
    def getBoardStr(self):
        """Return a text-representation of the board with hints."""
1         boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard.

        xCanWin = False
        oCanWin = False
2         originalSpaces = self._spaces # Backup _spaces.
        for space in ALL_SPACES: # Check each space:
            # Simulate X moving on this space:
            self._spaces = copy.copy(originalSpaces)
            if self._spaces[space] == BLANK:
                self._spaces[space] = X
            if self.isWinner(X):
                xCanWin = True
            # Simulate O moving on this space:
3             self._spaces = copy.copy(originalSpaces)
            if self._spaces[space] == BLANK:
                self._spaces[space] = O
            if self.isWinner(O):
                oCanWin = True
        if xCanWin:
            boardStr += '\nX can win in one more move.'
        if oCanWin:
            boardStr += '\nO can win in one more move.'
        self._spaces = originalSpaces
        return boardStr

أولًا، تنفذ التعليمة ‎‏super().getBoardStr()‎ في السطر ذو الرقم 1 الشيفرة داخل الصنف getBoardStr()‎ الخاص بصنف TTTBoard، والتي تعيد سلسلةً نصيةً على شكل لوحة إكس أو. نحفظ حاليًا هذه السلسلة في متغير اسمه boardStr. تعالج الشيفرة الباقية إنشاء التلميح بعد إنشاء لوحة السلسلة النصية عن طريق إعادة استخدام getBoardStr()‎ الخاص بصنف TTTBoard. يعيّن تابع getBoardStr()‎ قيمة المتغيرين xCanWin و oCanWin إلى False، وينسخ احتياطيًا القاموس self._spaces إلى المتغير ‎originalSpaces‎ (السطر ذو الرقم 2)، ثم تُنفَّذ حلقة for على كل أماكن اللوحة من 1 إلى 9. تُضبط سمة self._spaces لنسخ المكتبة originalSpaces، وإذا كانت الخلية فارغة تُوضع X مكانها، إذ يحفز هذا تحريك X إلى الفراغ التالي.

سيحدد استدعاء self.isWinner()‎ إذا كانت هذه هي الحركة الرابحة؛ فإذا كانت كذلك تصبح xCanWin هي True. تُكرر هذه الخطوات من أجل O لمعرفة ما إذا كان O يربح بالتحرك إلى هذا المكان (السطر ذو الرقم 3). يستخدم هذا التابع وحدة copy لنسخ القاموس في self._spaces لذا نضيف السطر التالي لأول ملف tictactoe.py.

import copy

نغير بعدها السطر في main()‎ لنستنسخ كائن HintBoard بدلًا من TTTBoard:

    gameBoard = HintBoard() # Create a TTT board object.

يعمل البرنامج كما كان عدا تغيير السطر الوحيد في main()‎ وعندما ينفذ البرنامج سيكون الخرج على النحو التالي:

Welcome to Tic-Tac-Toe!
--snip--
      X| |   1 2 3
      -+-+-
       | |O  4 5 6
      -+-+-
       | |X  7 8 9
X can win in one more move.
What is O's move? (1-9)
5

      X| |   1 2 3
      -+-+-
       |O|O  4 5 6
      -+-+-
       | |X  7 8 9
O can win in one more move.
--snip--
The game is a tie!
Thanks for playing!

في نهاية التابع: إذا كانت قيمة xCanWin و oCanWin هي True، تُضاف رسالةٌ إضافية تشير إلى ذلك إلى السلسلة النصية boardStr، وأخيرًا تُعاد القيمة boardStr.

لا يحتاج كل تابع معاد تعريفه لاستخدام super()‎؛ فإذا كان يعمل التابع -الذي يعيد التعريف- شيئًا مختلفًا تمامًا عن التابع المُعاد تعريفه في الصنف الأب، لا توجد حاجة لاستدعاء التابع المعاد تعريفه باستخدام super()‎. تفيد الدالة super()‎ على نحوٍ خاص عندما يكون للصنف أكثر من تابع أب كما موضح في الفقرة "الوراثة المتعددة" لاحقًا.

فضل التكون Composition على الوراثة

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

على الرغم من أنه بإمكانك استخدام الوراثة للأصناف ذات العلاقات " is a" (بمعنى آخر، عندما يكون الصنف الابن هو نوع من أنواع الصنف الأب)، من المفضل استخدام تقنية تدعى التكوّن composition للأصناف ذات العلاقات "لديه has a". التكوّن هو تقنية تصميم لضم الكائنات في الأصناف الخاصة بك بدلًا من توارث أصناف تلك الكائنات. هذا ما نفعله عندما نضيف خاصيّات إلى الأصناف الخاصة بنا.

عند تصميم الأصناف الخاصة بك باستخدام الوراثة فضّل التكوّن على الوراثة، هذا ما كنا نفعله في كل الأمثلة الحالية والسابقة كما يلي:

  • كائن WizCoin "لديه has a" كمية من النقود من أنواع galleon و sickle و knut.
  • كائن TTTBoard "لديه has a" مصفوفة بتسع فراغات.
  • كائن MiniBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات.
  • كائن HintBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات.

لنعد إلى صنف WizCoin الذي أنشأناه سابقًا. إذا أنشأنا صنف WizardCustomer لتمثل الزبائن في العالم السحري، يجب على هؤلاء الزبائن حمل كمية من المال، الذي نعبّر عنه بصنف WizCoin ولكن لا توجد علاقة "is a" بين الصنفين؛ فكائن WizardCustomer ليس من نوع كائن WizCoin. إذا استخدمنا الوراثة، سنحصل على شيفرة برمجية غير مُعتادة:

import wizcoin

1 class WizardCustomer(wizcoin.WizCoin):
    def __init__(self, name):
        self.name = name
        super().__init__(0, 0, 0)

wizard = WizardCustomer('Alice')
print(f'{wizard.name} has {wizard.value()} knuts worth of money.')
print(f'{wizard.name}\'s coins weigh {wizard.weightInGrams()} grams.')

في هذا المثال، يرث WizardCustomer توابع الكائن ‎‏WizCoin مثل value()‎ و weightInGrams()‎. تقنيًا يمكن للصنف WizardCustomer الذي ورث من WizCoin أن ينجز بجميع المهام التي ينجزها WizardCustomer، والتي تضم كائن WizCoin، كما تفعل السمة، ولكن اسمَي التابعين wizard.value()‎ و wizard.weightInGrams()‎ مضللة؛ إذ يبدو أنها تُعيد قيمة ووزن الساحر بدلًا من قيمة ووزن نقود الساحر. إضافةً إلى ذلك، إذا أردنا لاحقًا إضافة تابع weightInGrams()‎ لوزن الساحر، سيكون هذا الاسم مأخوذًا مسبقًا.

من الأسهل أن يكون الكائن WizCoin سمةً لأن الزبون الساحر "لديه" كميةً من نقود الساحر.

import wizcoin

class WizardCustomer:
    def __init__(self, name):
        self.name = name
1         self.purse = wizcoin.WizCoin(0, 0, 0)

wizard = WizardCustomer('Alice')
print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.')
print(f'{wizard.name}\'s coins weigh {wizard.purse.weightInGrams()} grams.')

بدلًا من جعل الصنف WizardCutomer يرث التوابع من WizCoin، نعطي للصنف WizardCutomer سمة ‎‏purse التي تحتوي كائن WizCoin. أي تغييرات لتوابع الصنف WizCoin عند استخدام التكوّن لن تغير توابع الصنف WizardCustomer. تمنحك هذه الطريقة مرونةً أكبر في تغيير التصميمات المستقبلية لكلا الصنفين وتؤدي إلى شيفرة سهلة الصيانة.

مساوئ الوراثة

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

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

ستكون شيفرة هذه الأصناف على النحو التالي:

class Vehicle:
    def __init__(self):
        print('Vehicle created.')
    def startIgnition(self):
        pass  # Ignition starting code goes here.
    def changeTire(self):
        pass  # Tire changing code goes here.

class Car(Vehicle):
    def __init__(self):
        print('Car created.')

class Motorcycle(Vehicle):
    def __init__(self):
        print('Motorcycle created.')

class LunarRover(Vehicle):
    def __init__(self):
        print('LunarRover created.')

لكن كل التغييرات المستقبلية على Vehicle ستؤثر على هذه الأصناف الفرعية أيضًا. ماذا سيحدث لو أردنا تابع changeSparkPlug()‎؟ لدى السيارات والدراجات النارية محركات احتراق بشمعات احتراق ولكن العربات القمرية lunar rovers ليس لديها ذلك. يمكننا -بتفضيل التكوّن على الوراثة- إنشاء صنفي CombustionEngine و ElectricEngine، ثم تصميم صنف Vehicle ليكون "لديه has a" سمة محرك إما CombustionEngine أو ElectricEngine مع التوابع الموافقة.

class CombustionEngine:
    def __init__(self):
        print('Combustion engine created.')
    def changeSparkPlug(self):
        pass  # هنا الشيفرة البرمجية التي تعدّل على شمعة الاحتراق 

class ElectricEngine:
    def __init__(self):
        print('Electric engine created.')

class Vehicle:
    def __init__(self):
        print('Vehicle created.')
        self.engine = CombustionEngine()  # استخدم هذا المحرك افتراضيًا
--snip--

class LunarRover(Vehicle):
    def __init__(self):
        print('LunarRover created.')
        self.engine = ElectricEngine()

يتطلب هذا إعادة كتابة كمية كبيرة من الشيفرة خصوصًا إذا كان لدينا عدة أصناف ترث من الصنف Vehicle الموجود مسبقًا. كل استدعاءات vehicleObj.changeSparkPlug()‎ ستكون بحاجة لتصبح vehicleObj.engine.changeSparkPlug()‎ لكل كائن في الصنف Vehicle أو أصنافها الفرعية لأن كل تغيير كبير سيحدث أخطاءً ربما تجعل من التابع changeSparkPlug()‎ الخاص بالصنف LunarVehicle لا يفعل شيئًا. تتمثل الطريقة الخاصة ببايثون في هذه الحالة بضبط قيمة changeSparkPlug إلى None في صنف LunarVehicle:

class LunarRover(Vehicle):
    changeSparkPlug = None
    def __init__(self):
        print('LunarRover created.')

يتبع السطر:

changeSparkPlug = None

الصياغة المعرفة في "سمات الصنف" التي سنناقشها لاحقًا، وهذا يعيد تعريف التابع changeSparkPlug()‎ الموروث من Vehicle، لذا يسبب استدعاؤه باستخدام كائن LunarRover خطأ:

>>> myVehicle = LunarRover()
LunarRover created.
>>> myVehicle.changeSparkPlug()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

يؤدي هذا الخطأ إلى فشل البرنامج وتوقفه سريعًا، ويمكننا مباشرةً ملاحظة المشكلة عند استدعاء التابع غير المناسب باستخدام كائن LunarRover. يرث أيضًا كل صنف ابن للصنف LunarRover القيمة None للتابع changeSparkPlug()‎.

تخبرنا رسالة الخطأ التالية بأن مبرمج الصنف LunarRover تعمّد ضبط قيمة التابع changSprakPlug()‎ إلى None:

TypeError: 'NoneType' object is not callable

إذا لم يكن هناك بالأساس تابع، سنحصل على رسالة الخطأ التالية:

NameError: name 'changeSparkPlug' is not defined

تخلق الوراثة أصنافًا فيها تعقيدات وتناقضات لذا يُفضل استخدام التكوّن بدلًا عنها.

الخلاصة

الوراثة هي تقنية لإعادة استخدام الشيفرة، تسمح لك إنشاء أصناف ابن التي ترث توابع أصناف الأب، يمكنك إعادة تعريف التوابع لتقدم شيفرةً جديدةً لهم واستخدام super()‎ لاستدعاء التابع الأصلي من الصنف الأب. لدى الأصناف الابن علاقة "is a" مع الصنف الأب الخاصة بها، لأن كائن من الصنف الابن هو كائن للصنف الأب.

استخدام الأصناف والوراثة في بايثون اختياري، إذ يرى بعض المبرمجين أن التعقيد المرافق للاستخدام الكثير للوراثة لا يبرر فائدته. من المرونة أكثر استخدام التكوّن بدلًا من الوراثة لأنها تنفذ علاقة "has a" مع كائن من أحد الأصناف وكان من أصناف أخرى بدلًا من وراثة التوابع مباشرةً من هذه الأصناف، فمثلًا قد يحتوي كائن Customer على سمة birthday المسندة إلى كائن Date بدلًا من أن يكون هناك أصناف فرعية من صنف Customer للكائن Date.

ترجمة -وبتصرف- لقسم من الفصل 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.


×
×
  • أضف...