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


الدرس: اكتشف الفوقية (metaclasses)


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

تعليقات على عملية إنشاء مثيل



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

أخبرتك حينها أن الأمور كانت أكثر تعقيدًا مما تبدو عليه. سنرى الآن كيف!

لنفترض أنك حددت فئة:


class Personne:
    
    """ فئة تحدد الشخص.
    
     لها سمات:
    nom – اسم الشخص
    prenom – اسمه الاول
    age -- عمره
    lieu_residence – مكان اقامته
    
    يجب تمرير الاسم الأول والأخير للمنشئ."""
    
    def __init__(self, nom, prenom):
        """ منشئ شخصنا."""
        self.nom = nom
        self.prenom = prenom
        self.age = 23
        self.lieu_residence = "Lyon"
 
هذه الصيغة ليست جديدة بالنسبة لنا.

الآن ماذا يحدث عندما تريد إنشاء شخص؟ سهل نكتب الكود التالي:


personne = Personne("Doe", "John")
 
عندما نفعل ذلك ، تستدعي Python المُنشئ الخاص بنا __init__، وتمرره إلى الحجج المقدمة لبناء الكائن. ومع ذلك ، هناك خطوة وسيطة.

إذا ألقيت نظرة على تعريف المُنشئ الخاص بنا:


def __init__(self, nom, prenom):
 
ألا تلاحظ أي شيء غريب؟ ربما لا ، لأنك اعتدت على بناء الجملة هذا منذ بداية هذا الجزء: تأخذ الطريقة كمعامل أول self .

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

بطريقة ما ، تنشئ الفئة الخاصة بنا كائنًا جديدًا ويمرره إلى المنشئ الخاص بنا. الطريقة __init__مسؤولة عن كتابة سماتها في كائننا ، لكنها ليست مسؤولة عن إنشاء كائننا. سنرى الآن من يفعل ذلك.

الطريقة__new__


الطريقة __init__، كما رأينا ، موجودة لتهيئة كائننا (عن طريق كتابة سمات فيه ، على سبيل المثال) ولكنها ليست موجودة لإنشائها . الطريقة التي تعتني بها هي __new__ .

إنها أيضًا طريقة خاصة ، تدرك خصوصيتها. إنها أيضًا طريقة محددة بواسطة object ، والتي يمكن إعادة تعريفها إذا لزم الأمر.

قبل أن نرى ما يتطلبه الأمر في المعلمات ، دعنا نرى بدقة أكبر ما يحدث عندما نحاول بناء كائن:

  • نطلب إنشاء كائن ، عن طريق الكتابة على سبيل المثال Personne("Doe", "John") .
  • يتم استدعاء طريقة  الفئة __new__ (هنا Personne ) وتهتم ببناء كائن جديد.
  • إذا __new__أعادنا مثيلًا من الفئة ، فإننا نطلق على المُنشئ __init__عن طريق تمريره كمعلمات إلى هذا المثيل الجديد بالإضافة إلى الوسائط التي تم تمريرها أثناء إنشاء الكائن.
الآن ، دعنا نلقي نظرة على بنية طريقتنا __new__ .

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

لذلك لا يأخذ self كمعامل أول (مثيل الكائن). ومع ذلك ، فهي تأخذ الطبقة التي تم معالجتها  cls .

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

سيتم تمرير المعلمات الأخرى التي تم تمريرها إلى الطريقة __new__إلى المنشئ.

دعنا نلقي نظرة على هذا ، معبرًا عنه ككود:


class Personne:
    
    """ فئة تحدد الشخص.
    
     لها سمات:
    nom – اسم الشخص
    prenom – اسمه الاول
    age -- عمره
    lieu_residence – مكان اقامته
    
    يجب تمرير الاسم الأول والأخير للمنشئ."""
    
    def __new__(cls, nom, prenom):
        print("استدعاء طريقة __new__ للفئة {}".format(cls))
        return object.__new__(cls, nom, prenom)
    
    def __init__(self, nom, prenom):
        """ منشئ شخصنا."""
        print("استدعاء الطريقة __init__ ")
        self.nom = nom
        self.prenom = prenom
        self.age = 23
        self.lieu_residence = "Lyon"
 
دعنا نحاول إنشاء شخص:

>>> personne = Personne("Doe", "John")
Appel de la méthode __new__ de la classe <class '__main__.Personne'>
Appel de la méthode __init__
>>>
 
يمكن أن تسمح إعادة التعريف__new__ ، على سبيل المثال ، بإنشاء مثيل لفئة أخرى. يتم استخدامها بشكل أساسي بواسطة Python لإنتاج الأنواع غير القابلة للتغيير  (باللغة الإنجليزية ، immutable)، والتي لا يمكن تغييرها ، مثل السلاسل ، والصفوف ، والأعداد الصحيحة ، والعائمة ...

يتم إعادة تعريف الطريقة __new__أحيانًا في جسم metaclass . سنرى الآن ما هو عليه.

قم بإنشاء فئة ديناميكية



أكرر مرة أخرى ، كل شيء في بايثون هو كائن . هذا يعني أن الأعداد الصحيحة ، والعوامات ، والقوائم هي كائنات ، وأن الوحدات هي كائنات ، وأن الحزم هي كائنات ... ولكنه يعني أيضًا أن الفئات هي كائنات!

الطريقة التي نعرفها


لإنشاء فئة ، رأينا طريقة واحدة فقط ، وهي الأكثر استخدامًا ، وهي استدعاء الكلمة الأساسية  class .


class MaClasse:
 
يمكنك بعد ذلك إنشاء أمثلة على نموذج هذه الفئة ، فأنا لا أعلمك أي شيء.

ولكن الأمر المعقد هو أن الفئات هي أيضًا كائنات.

إذا كانت الفئات عبارة عن كائنات ... فهل هذا يعني أن الفئات نفسها تم تصميمها وفقًا للفئات؟

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


>>> type(5)
<class 'int'>
>>> type("une chaîne")
<class 'str'>
>>> type([1, 2, 3])
<class 'list'>
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(list)
<class 'type'>
>>>
 
نسأل عن نوع العدد الصحيح وتجيب بايثون class int . بدون مفاجأة. ولكن إذا طلبنا فئة int، تجيبنا بايثون class type .

في الواقع ، بشكل افتراضي ، يتم تصميم جميع فئاتنا على غرار الفئةtype . هذا يعني أن :

  • 1. عندما نقوم بإنشاء فئة جديدة (  class Personne :على سبيل المثال) ، تستدعي Python طريقة __new__للفئة type؛
  • 2. بمجرد إنشاء الفئة ، نستدعي منشئ __init__للفئة type .
ربما لا يزال يبدو غامضا. لا تيأس ، قد تفهم ما أتحدث عنه بشكل أفضل قليلاً بينما تقرأ الباقي. إذا لم يكن كذلك ، فلا تتردد في إعادة قراءة هذا المقطع واختباره بنفسك.

قم بإنشاء فئة ديناميكيًا


دعنا نلخص:

  • نحن نعلم أن الكائنات يتم تشكيلها على أساس الفئات ؛
  • نحن نعلم أن فئاتنا ، كونها كائنات ، تم تصميمها على غرار الفئة ؛
  • يتم استدعاء الفئة التي يصمم عليها الآخرون بشكل افتراضي type .
أقترح عليك محاولة إنشاء فئة ديناميكيًا ، دون المرور بالكلمة الرئيسية class ولكن من خلال الفئة type مباشرة.

تأخذ الفئة  Type ثلاث حجج لبناء نفسها:

  • اسم الفئة المراد إنشاؤها ؛
  • و الصفوف (tuple) التي تحتوي الفئات التي سترث منها فئاتنا الجديدة ؛
  • قاموس يحتوي على سمات وطرق فئتنا.

>>> Personne = type("Personne", (), {})
>>> Personne
<class '__main__.Personne'>
>>> john = Personne()
>>> dir(john)
['__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__g
e__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__',
'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '_
_setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>>  
لقد قمت بتبسيط الكود قدر الإمكان. نقوم بإنشاء فئة جديدة نقوم بتخزينها في المتغير الخاص بنا Personne ، لكنها فارغة. لا يرث أي فئة ولا يحدد أي سمات أو طرق للفئة.

سنحاول إنشاء طريقتين لفصلنا:

  • المنشئ  __init__ .
  • طريقة presenter عرض الاسم الأول والأخير للشخص.
أقدم لكم هنا الكود الذي يمكننا الوصول إليه:

def creer_personne(personne, nom, prenom):
    """ الوظيفة التي ستلعب دور المُنشئ لفئة الأشخاص لدينا.
    
     يأخذ كمعامل ، بالإضافة إلى الشخص:
    nom -- اسمه
    prenom – اسمه الاول"""
    
    personne.nom = nom
    personne.prenom = prenom
    personne.age = 21
    personne.lieu_residence = "Lyon"

def presenter_personne(personne):
    """ وظيفة تقديم الشخص.
    
    تعرض اسمها الأول والأخير """
    
    print("{} {}".format(personne.prenom, personne.nom))

# قاموس الطرق
methodes = {
    "__init__": creer_personne,
    "presenter": presenter_personne,
}

# إنشاء فئة ديناميكية
Personne = type("Personne", (), methodes)
 
قبل رؤية التفسيرات ، دعنا نرى التأثيرات:


>>> john = Personne("Doe", "John")
>>> john.nom
'Doe'
>>> john.prenom
'John'
>>> john.age
21
>>> john.presenter()
John Doe
>>>
 
لن أخفيها عنك ، فهذه ميزة ربما تستخدمها نادرًا جدًا. لكن هذا التفسير كان مناسبًا عندما كنا مهتمين بالفئات الفوقية.

الآن ، دعنا نقسم الكود الخاص بنا:

  1. نبدأ بإنشاء وظيفتين ، creer_personne و presenter_personne . لها أن تصبح الطرق  __init__و presenter من فئتنا المستقبلية. نظرًا لكونها عمليات مثيل مستقبلية ، يجب أن تأخذ الكائن الذي تتم معالجته كمعامل أول.
  2. نضع هاتين الوظيفتين في قاموس. المفتاح هو اسم الطريقة المستقبلية والقيمة ، الوظيفة المقابلة.
  3. أخيرًا ، ندعوها type بتمريرها ، في المعلمة الثالثة ، القاموس الذي أنشأناه للتو.
إذا حاولت وضع سمات في هذا القاموس type الذي تم تمريره ، فيجب أن تدرك أن هذه ستكون سمات فئة ، وليست سمات مثيل.

تعريف metaclass



لقد رأينا أن هذا type هو metaclass لجميع الفئات الافتراضية. ومع ذلك ، يمكن للفئة أن تحتوي على metaclass آخر من type .

بناء metaclass هو نفس بناء الفئة. الفوقية ترث من type . سنجد الهيكل الأساسي للفئات التي رأيناها من قبل.

سنكون مهتمين بشكل خاص بطريقتين استخدمناهما في تعريفات الفئة لدينا:

  • الطريقة __new__، وتُستدعى لإنشاء فئة ؛
  • الطريقة __init__، وتُستدعى لبناء الفئة.

الطريقة__new__


تتطلب أربع معلمات:

  • كانت metaclass بمثابة الأساس لإنشاء صئتنا الجديدة ؛
  • اسم فئتنا الجديدة ؛
  • و الصفوف (tuple) التي تحتوي على الفئات التي ورثتها فئتنا المراد إنشاؤها.
  • معجم السمات وطرق إنشاء الفئة.
يجب أن تتعرف على المعلمات الثلاثة الأخيرة: وهي نفس تلك التي تم تمريرها إليها type .

هذه طريقة __new__بسيطة.


class MaMetaClasse(type):
    
    """ مثال على metaclass."""
    
    def __new__(metacls, nom, bases, dict):
        """انشاء فئتنا."""
        print("ننشئ الفئة {}".format(nom))
        return type.__new__(metacls, nom, bases, dict)
 
للقول أن الفئة تأخذ شيئًا آخر غير فئة metaclass type ، فإنه يحدث في سطر تعريف الفئة:

class MaClasse(metaclass=MaMetaClasse):
    pass
 
من خلال تشغيل هذا الكود ، يمكنك رؤية:


ننشئ الفئة  MaClasse
 

الطريقة__init__


يأخذ مُنشئ metaclass نفس المعلمات __new__، باستثناء الأولى ، التي لم تعد metaclass تعمل كنموذج ولكن الفئة التي أنشأناها للتو.

لا تزال المعلمات الثلاث التالية هي نفسها: الاسم، و الصفوف (tuple) من الفئات الأم، والقاموس من سمات الفئة والأساليب.

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

الآن دعنا نرى ما يمكن استخدامه من أجله.

الفوقية(metaclass) عمليا


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

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

سنحاول الاحتفاظ بالفئات التي أنشأناها في قاموس مع الأخذ في الاعتبار اسم الفئة وكقيمة للفئة نفسها.

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

على سبيل المثال ، سيتم استدعاء الفئة الأصلية لجميع أدواتنا Widget . سترث هذه الفئة الفئات Bouton، CaseACocher، Menu، Cadre، إلخ. سيتمكن مستخدم المكتبة أيضًا من اشتقاق دروسه الخاصة منها.

القاموس الذي نرغب في إنشائه يبدو كالتالي:


{
    "Widget": Widget,
    "Bouton": Bouton,
    "CaseACocher": CaseACocher,
    "Menu": Menu,
    "Cadre": Cadre,
    ...
}
 
يمكن ملء هذا القاموس يدويًا في كل مرة نقوم فيها بإنشاء فئة موروث منها Widget ولكن نعترف أنه لن يكون عمليًا للغاية.

في هذا السياق ، يمكن أن تجعل الفوقية حياتنا أسهل. يمكنك محاولة القيام بالتمرين ، فالكود ليس معقدا للغاية. بعد قولي هذا ، نظرًا لأننا رأينا الكثير في هذه الفئة وأن metaclasses هي مفهوم متقدم نوعًا ما ، فأنا أقدم لك الكود على الفور الذي قد يساعدك على فهم الآلية:


trace_classes = {} # Notre dictionnaire vide

class MetaWidget(type):
    
    """ ميتاكلاس لدينا لأدواتنا.
    
     يرث من النوع ، لأنه ميتاكلاس.
     ستكتب في قاموس trace_classes في كل مرة
     أنه سيتم إنشاء فصل دراسي ، باستخدام هذه الفئة الفوقية بشكل طبيعي. """    
    def __init__(cls, nom, bases, dict):
        """ يُطلق على مُنشئ metaclass الخاص بنا عندما نقوم بإنشاء فصل دراسي."""
        type.__init__(cls, nom, bases, dict)
        trace_classes[nom] = cls
 
ليست معقدة للغاية في الوقت الحالي. لنقم بإنشاء فئتنا Widget :

class Widget(metaclass=MetaWidget):
    
    """Classe mère de tous nos widgets."""
    
    Pass
 
بعد تشغيل هذا الكود ، يمكنك ملاحظة أن فئتنا Widget تمت إضافتها إلى قاموسنا:


>>> trace_classes
{'Widget': <class '__main__.Widget'>}
>>>
 
الآن دعونا نبني فئة جديدة موروثة من Widget .

class bouton(Widget):
    
    """ فئة تحدد أداة الزر."""
    
    Pass
 
إذا قمت بعرض محتويات القاموس مرة أخرى ، فسترى أنه Bouton قد تمت إضافة الفئة . وراثة من Widget ، تستخدم نفس الفئة الوصفية (ما لم ينص صراحة على خلاف ذلك) وبالتالي تتم إضافتها إلى القاموس.

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

الاستنتاج


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

باختصار


  • يتم تنفيذ عملية إنشاء كائن من خلال طريقتين ، __new__و __init__ .
  • __new__هو المسؤول عن إنشاء الكائن ويأخذ فئته كمعامله الأول.
  • __init__مسؤول عن تهيئة سمات الكائن ويأخذ باعتباره المعلمة الأولى الكائن الذي تم إنشاؤه مسبقًا بواسطة __new__ .
  • نظرًا لأن الفئات عبارة عن كائنات ، فقد تم تصميمها جميعًا في فئة تسمى metaclass  .
  • ما لم يتم تغييره بشكل صريح ، فإن metaclass لجميع الفئات هو type .
  • يمكن للمرء أن يستخدم type لإنشاء الطبقات بشكل ديناميكي.
  • يمكنك أن ترث فئة من type لإنشاء فئة تعريف جديدة.
  • في الجسم من فئة، لتحديد metaclass ، فإنه يعمل بناء الجملة التالي: class MaClasse(metaclass=NomDeLaMetaClasse): .