تعلم البرمجة مع Python


الدرس: إدارة الوراثة (inheritances)


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

ما فائدة هذه الميزة؟

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

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

للبدء بشكل جيد



لن أجعل التشويق يستمر بعد الآن: الوراثة هي وظيفة كائن تسمح لك بإعلان أن هذه الفئة نفسها سيتم تصميمها على فئة أخرى ، والتي نسميها الفئة الأصل ، أو الفئة الرئيسية . بشكل ملموس ، إذا b ورثت فئة من الفئة a، فإن الكائنات التي تم إنشاؤها في نموذج الفصل b ستتمكن من الوصول إلى أساليب وسمات الفئة a .

وهذا كل شيء ؟ هذا عديم الفائدة!

لا ، هذا ليس كل شيء ، وإذا كان مفيدًا جدًا ، لكن سيتعين عليك إعطائي بعض الوقت لأظهر لك الاهتمام.

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

لنأخذ مثالًا بسيطًا: لدينا فئة Animal تسمح بتعريف الحيوانات. الحيوانات كما نعرضها لها سمات معينة (النظام الغذائي: آكلة اللحوم أو العاشبة) وطرق معينة (الأكل والشرب والصراخ ...).

يمكننا الآن تحديد فئة Dog ترث منها Animal، أي أنها تأخذ أساليبها. سنرى أدناه بالضبط ما يعنيه هذا.

إذا كنت لا ترى جيدًا في هذه الحالة ، يتم توريث فئة من فئة أخرى ، فقم بإجراء الاختبار:

  • ترث الفئة Dog من  Animalلان  الكلب حيوان .
  • لا ترث Animal من Dog لأن Animal ليس  Dog .
في هذا النموذج ، يمكنك أن ترى أن السيارة هي مركبة . Carوبالتالي يمكن للفئة أن ترث من Vehicle .

الآن دعونا نلقي نظرة على الكود.

الميراث البسيط



الميراث البسيط ، الجوانب النظرية التي رأيناها للتو في القسم السابق ، تعارض الميراث المتعدد الذي سنراه في القسم التالي.

حان الوقت للوصول إلى صيغة الميراث. سنحدد الدرجة الأولى والفئة A الثانية B التي ترث منها A .


class A:
    """ الفئة A  ، لتوضيح مثالنا عن الميراث """
    pass # نترك التعريف فارغًا ، هذا مجرد مثال

class B(A):
    """Classe B, qui hérite de A.
    A يستخدم نفس الأساليب والسمات (في هذا المثال ، الفئة
    ليس لديها أي طريقة أو سمة على أي حال)"""
    
    Pass
 
يمكنك تجربة المزيد من الأمثلة البناءة لاحقًا. في الوقت الراهن، والشيء المهم هو أن نلاحظ بناء الجملة، وكما ترون، بسيط جدا: class MaClasse(MaClasseMere): . في تعريف الفئة ، بين الاسم والنقطتين ، تحدد بين أقواس الفئة التي يجب أن ترث منها. كما قلت ، في البداية ، سيتم A العثور على جميع أساليب الفئة في الفئة B .

حاولت أن أضع مُنشئين في الفئتين ولكنني لا أستطيع أن أجد الصفات المعلنة في فئتي الأم في فئة البنت ، هل هذا طبيعي؟

تماما. أتذكر عندما أخبرتك أن العمليات معرّفة في الفئة ، بينما يتم التصريح عن السمات مباشرة في مثيل الكائن؟ يمكنك رؤيته على أي حال: في المُنشئ نعلن السمات ونكتبها جميعًا في المثال self .

عندما B ترث فئة من فئة A، فإن كائنات الكتابة B تستولي بالفعل على طرق الفئة A في نفس الوقت مثل تلك الخاصة بالفئة B . ولكن ، منطقيًا بما فيه الكفاية ، B يتم استدعاء هؤلاء من الفئة أولاً.

إذا قمت بذلك objet_de_type_b.ma_methode() ، فستبحث Python أولاً عن الطريقة ma_methode في الفئة B التي يتم اشتقاق الكائن منها مباشرةً. إذا لم يتم العثور عليها ، فسيتم البحث بشكل متكرر في الفئات التي يرثها B، أي A في مثالنا. هذه الآلية مهمة جدًا: فهي تعني أنه إذا لم يتم إعادة تعريف أي طريقة في الفئة ، فإننا ننظر في الفئة الأصل. يمكننا بالتالي إعادة تعريف طريقة معينة في الفئة والسماح للآخرين بأن يرثوا مباشرة من الطبقة الأصل.

كود مثال صغير:


class Personne:
    """ فئة تمثل شخصًا """
    def __init__(self, nom):
        """منشئ الفئة"""
        self.nom = nom
        self.prenom = "Martin"
    def __str__(self):
        """ يتم استدعاء الأسلوب عند تحويل الكائن إلى سلسلة """
        return "{0} {1}".format(self.prenom, self.nom)

class AgentSpecial(Personne):
    """ فئة تحدد وكيل خاص.
    يرث من فئة الشخص """
    
    def __init__(self, nom, matricule):
        """ يتم تعريف الوكيل باسمه ورقمه """
        self.nom = nom
        self.matricule = matricule
    def __str__(self):
        """ يتم استدعاء الأسلوب عند تحويل الكائن إلى سلسلة """
        return "Agent {0}, matricule {1}".format(self.nom, self.matricule)
 
هنا ترى مثالا على الميراث البسيط. فقط ، إذا حاولت إنشاء وكلاء خاصين ، فقد تواجه بعض المفاجآت المضحكة:

>>> agent = AgentSpecial("Fisher", "18327-121")
>>> agent.nom
'Fisher'
>>> print(agent)
Agent Fisher, matricule 18327-121
>>> agent.prenom
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: 'AgentSpecial' object has no attribute 'prenom'
>>>
 
... لكنك لم تقل أن الفئة تأخذ أساليب وسمات فئتها الأصلية؟

نعم ، ولكن باتباع التنفيذ جيدًا ، ستفهم: كل شيء يبدأ بإنشاء الكائن.  سيقع استدعاء أيّ منشئ ؟ إذا لم يكن هناك مُنشئ محدد في فئتنا AgentSpecial ، فإن Python ستطلق عليه اسم Personne . ولكن يوجد بالفعل واحد في الفئة AgentSpecial ولذلك يسمى هذا. في هذا المنشئ ، نحدد سمتين ، nom و matricule . لكن هذا كل شيء:  Personne لا يتم استدعاء مُنشئ الفئة إلا إذا استدعيته صراحة في المُنشئ AgentSpecial .

في الفئة الأولى ، شرحت لك أنه mon_objet.ma_methode() عاد إلى نفس MaClasse.ma_methode(mon_objet) . في طريقتنا ma_methode، self ستكون المعلمة الأولى mon_objet . سوف نستخدم هذا التكافؤ. في معظم الأحيان ، الكتابة mon_objet.ma_methode() كافية. لكن في علاقة الوراثة ، يمكن أن تكون هناك ، كما رأينا ، عدة طرق تحمل الاسم نفسه محددة في فئات مختلفة. أي واحد للاتصال؟ تختار Python ، إذا وجدت ذلك ، الشخص المحدد مباشرة في الفئة التي يأتي منها الكائن ، وإلا فإنها تتجاوز التسلسل الهرمي للميراث حتى تأتي عبر الطريقة. ولكن يمكنك أيضًا استخدام الكود MaClasse.ma_methode(mon_objet) لاستدعاء طريقة معينة لفئة معينة. وهذا مفيد في حالتنا:


class Personne:
    """ فئة تمثل شخصًا """
    def __init__(self, nom):
        """ منشئ فئتنا """
        self.nom = nom
        self.prenom = "Martin"
    def __str__(self):
        """ يتم استدعاء الأسلوب عند تحويل الكائن إلى سلسلة """
        return "{0} {1}".format(self.prenom, self.nom)

class AgentSpecial(Personne):
    """ فئة تحدد وكيلا خاصا.
    يرث من فئة الشخص """
    
    def __init__(self, nom, matricule):
        """ يتم تعريف الوكيل باسمه ورقمه """
        # نطلق صراحة على الشخص المُنشئ:
        Personne.__init__(self, nom)
        self.matricule = matricule
    def __str__(self):
        """ يتم استدعاء الأسلوب عند تحويل الكائن إلى سلسلة """
        return "Agent {0}, matricule {1}".format(self.nom, self.matricule)
 
إذا كان هذا لا يزال يبدو غامضًا بعض الشيء بالنسبة لك ، فجرّب: لا تزال هذه هي أفضل طريقة. تدرب أو تحكم في كتابة السمات أو ارجع إلى الفصل الأول من هذا الجزء لتحديث ذاكرتك بشأن المعلمة self، على الرغم من أنك بفعل التجارب يجب أن تكون قد فهمت الفكرة.

دعنا نعود إلى الكود الخاص بنا من قبل والذي ، هذه المرة ، يمر دون مشكلة:


>>> agent = AgentSpecial("Fisher", "18327-121")
>>> agent.nom
'Fisher'
>>> print(agent)
Agent Fisher, matricule 18327-121
>>> agent.prenom
'Martin'
>>>
 
هذه المرة ، السمة prenom الخاصة بنا موجودة في وكيلنا الخاص لأن منشئ الفئة AgentSpecial يستدعي صراحةً ذلك لـ Personne .

قد تلاحظ أيضًا أنه في مُنشئ AgentSpecial السمة لا يتم إنشاء مثيل لها nom . هذا في الواقع مكتوب من قبل مُنشئ الفئة Personne التي نسميها بتمرير اسم وكيلنا كمعامل.

لاحظ أنه يمكننا أن نرث فئة جديدةً من فئتنا Personne، وغالبًا ما تكون الفئة نموذجًا للعديد من فصول الأطفال.

توضيح


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


def __setattr__(self, nom_attribut, valeur_attribut):
        """  objet.attribut = valeur يتم استدعاء الأسلوب عند تكوين الكائن """
        print("Attention, on modifie l'attribut {0} de l'objet !".format(nom_attribut))
        object.__setattr__(self, nom_attribut, valeur_attribut)
 
من خلال إعادة تعريف الطريقة __setattr__، لا يمكننا ، في جسم هذه الطريقة ، تعديل قيم سماتنا كما نفعل عادةً ( self.attribut = valeur) لأن الطريقة ستطلق على نفسها بعد ذلك. لذلك فإننا ندعو إلى طريقة __setattr__الطبقة object، هذه الفئة التي ترث منها جميع طبقاتنا ضمنيًا. نحن على يقين من أن طريقة هذه الفئة تعرف كيفية كتابة قيمة في سمة ، بينما نتجاهل الآلية ولا نحتاج إلى معرفتها: هذا هو سحر العملية ، بمجرد أن يكون لدينا فهمت المبدأ!

وظيفتان عمليتان للغاية


تحدد Python وظيفتين يمكن أن تكونا مفيدتين في كثير من الحالات:  issubclass و isinstance .

issubclass


كما يوحي الاسم ، فإنه يتحقق مما إذا كانت الفئة فئة فرعية من فئة أخرى. يعيد True إذا كان الأمر كذلك ، False عكس ذلك:


>>> issubclass(AgentSpecial, Personne) # Personne يرث من AgentSpecial
True
>>> issubclass(AgentSpecial, object)
True
>>> issubclass(Personne, object)
True
>>> issubclass(Personne, AgentSpecial) # AgentSpecial لا يرث من Personne
False
>>>
 

isinstance


Isinstance  يسمح لك بمعرفة ما إذا كان الكائن يأتي من فئة أو من فئة الفرعية:


>>> agent = AgentSpecial("Fisher", "18327-121")
>>> isinstance(agent, AgentSpecial) # Agent est une instance d'AgentSpecial
True
>>> isinstance(agent, Personne) # Agent est une instance héritée de Personne
True
>>>
 
أعتقد أن هذه الأمثلة القليلة كافية. قد تضطر إلى الانتظار قليلاً لتجد استخدامًا لكلتا هاتين الوظيفتين ، ولكن سيأتي ذلك الوقت.

الميراث المتعدد



تتضمن بايثون آلية للوراثة المتعددة . الفكرة في الأساس بسيطة للغاية: بدلاً من أن ترث من فئة واحدة ، يمكنك أن ترث من العديد منها.

أليس هذا ما يحدث عندما ترث من فئة ترث نفسها من فئة أخرى؟

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

يمكنك الجلوس على كرسي. يمكنك النوم في السرير. ولكن هناك بعض الأرائك التي يمكنك الجلوس والنوم عليها (معظمها ، في الواقع ، بقليل من حسن النية) . فئتنا (كرسي) Fauteuil سوف تكون قادرة على وراثة من فئة ObjetPourSAsseoir(مكان للجلوس) وصفنا (سرير) Lit، من طبقتنا ObjetPourDormir(مكان للنوم) . لكن صفنا  (كنبة)Canape إذن؟ يجب أن ترث منطقيًا من فئتينا ObjetPourSAsseoir و ObjetPourDormir . هذه هي الحالة التي قد يكون فيها الميراث المتعدد مفيدًا.

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

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


class MaClasseHeritee(MaClasseMere1, MaClasseMere2):
 
يمكنك أن ترث فئتك من أكثر من فئتين آخرين. بدلاً من تحديد ، كما في حالة الوراثة البسيطة ، فئة أصل واحد بين أقواس ، يمكنك تحديد عدة فئات ، مفصولة بفواصل.

طريقة البحث


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

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

مراجعة للاستثناءات



منذ الجزء الأول ، لم نعد إلى الاستثناءات. ومع ذلك ، يمنحني هذا الفصل فرصة للمضي قدمًا.

الاستثناءات ليست فئات فحسب ، بل فئات مرتبة حسب علاقة وراثية دقيقة.

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

لكن كيف تعرف أن الاستثناء يرث استثناءات أخرى؟

هناك احتمالات كثيرة. إذا كنت مهتمًا باستثناء معين ، فراجع المساعدة الخاصة به.


Help on class AttributeError in module builtins:
class AttributeError(Exception)
 |  Attribute not found.
 |
 |  Method resolution order:
 |      AttributeError
 |      Exception
 |      BaseException
 |      object
 
هنا تتعلم أن الاستثناء  AttributeError يرث من Exception الذي يرث نفسه من BaseException .

يمكنك أيضا العثور على هرم المدمج في استثناءات على موقع بايثون .

فقط ما يسمى بالاستثناءات المضمنة مذكورة هنا . يمكن تعريف البعض الآخر في وحدات ستستخدمها ويمكنك حتى إنشائها بنفسك (سنرى هذا قليلاً أدناه).

في الوقت الحالي ، تذكر أنك عندما تكتب except TypeException، ستتمكن من التقاط جميع استثناءات النوع TypeException ولكن أيضًا للفئات الموروثة منها TypeException .

يتم طرح معظم الاستثناءات للإبلاغ عن خطأ ... ولكن ليس كلها. يظهر الاستثناء KeyboardInterupt عند إيقاف البرنامج ، على سبيل المثال مع CTRL+ C . لذلك ، عندما نريد اكتشاف جميع الأخطاء المحتملة ، سنتجنب كتابة بسيطة  except: واستبدالها except Exception: ، حيث يتم اشتقاق جميع استثناءات "الخطأ" من Exception .

إنشاء استثناءات مخصصة


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

ضع نفسك في التسلسل الهرمي


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

  • BaseException : الفئة الأصلية لجميع الاستثناءات. في معظم الأحيان ، إذا كانت ترث فئتك منه BaseException ، فسيكون ذلك بمثابة نموذج لاستثناء ليس خطأ في الأساس ، على سبيل المثال انقطاع في معالجة برنامجك.
  • Exception : هذه هي الفئة التي ترثها استثناءاتك في معظم الأوقات. إنها الفئة الأصلية لجميع استثناءات "الخطأ".
إذا تمكنت من العثور على استثناء في السياق الأدنى في التسلسل الهرمي ، فهذا أفضل دائمًا.

ماذا يجب أن تحتوي فئتنا exception؟

شيئين: مُنشئ وطريقة __str__لأنه في وقت طرح الاستثناء ، يجب عرضه. في كثير من الأحيان ، يأخذ المُنشئ رسالة الخطأ فقط كمعامل __str__وتعيد الطريقة هذه الرسالة:


class MonException(Exception):
    """ أثير الاستثناء في سياق معين ... الذي لم يتم تحديده بعد """
    def __init__(self, message):
        """ نقوم فقط بتخزين رسالة الخطأ """
        self.message = message
    def __str__(self):
        """ نرسل الرسالة مرة أخرى """
        return self.message
 
هذا الاستثناء هو الأكثر استخدامًا في العالم:

>>> raise MonException("OUPS... j'ai tout cassé")
Traceback (most recent call last):
  File "", line 1, in 
__main__.MonException: OUPS... j'ai tout cassé
>>>
 
لكن يمكن أن تأخذ استثناءاتك أيضًا عدة معلمات عند إنشاء مثيل لها:

class ErreurAnalyseFichier(Exception):
    """يظهر هذا الاستثناء عند وجود ملف (تكوين)
     لا يمكن تحليلها.
    
     السمات:
         ملف - اسم الملف الإشكالي
         الخط - رقم خط المشكلة
         رسالة - المشكلة نفسها"""
    
    def __init__(self, fichier, ligne, message):
        """ منشئ استثناءنا """
        self.fichier = fichier
        self.ligne = ligne
        self.message = message
    def __str__(self):
        """ عرض الاستثناء """
        return "[{}:{}]: {}".format(self.fichier, self.ligne, \
                self.message)
 
ولإثارة هذا الاستثناء:

>>> raise ErreurAnalyseFichier("plop.conf", 34,
...         "Il manque une parenthèse à la fin de l'expression")
Traceback (most recent call last):
  File "", line 2, in 
__main__.ErreurAnalyseFichier: [plop.conf:34]: il manque une parenthèse à la fin de l'expression
>>>
 
إليكم الأمر ، اكتملت هذه المراجعة الصغيرة للاستثناءات. إذا كنت تريد معرفة المزيد ، فلا تتردد في مراجعة وثائق Python حول الاستثناءات بالإضافة إلى تلك المتعلقة بالاستثناءات المخصصة .

باختصار



  • يسمح الميراث للفئة بأن ترث سلوك آخر من خلال الاستيلاء على أساليبها.
  • صيغة الميراث هي class NouvelleClasse(ClasseMere): .
  • يمكن للمرء الوصول إلى طرق الطبقة الفائقة مباشرة عبر النحو ClasseMere.methode(self) .
  • تسمح الوراثة المتعددة للفئة بأن ترث من عدة فئات رئيسية.
  • ولذلك كتابة بناء الجملة من أجل وراثة متعددة على النحو التالي: class NouvelleClasse(ClasseMere1, ClasseMere2, ClasseMereN): .
  • يتم ترتيب الاستثناءات التي تحددها Python وفقًا للتسلسل الهرمي للوراثة.