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


الدرس: القبض على الديكور


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

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

ما هذا ؟



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

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

يمكن أن يكون هناك العديد من الحالات التي يكون فيها الديكور اختيارًا مثيرًا للاهتمام. لفهم الفكرة ، سأأخذ مثالًا واحدًا.

نريد اختبار أداء بعض وظائفنا ، وفي هذه الحالة ، حساب المدة التي تستغرقها للتشغيل.

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

الاحتمال الآخر هو استخدام المصمم. سيهتم هذا المصمم (decorator) بتنفيذ وظيفتنا عن طريق حساب الوقت الذي يستغرقه ويمكنه ، على سبيل المثال ، عرض تنبيه إذا كانت هذه المدة طويلة جدًا.

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

وهذا مجرد مثال على التطبيق.

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

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

نظريا



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

أبسط شكل


كما قلت ، المصممون (decorators) وظائف "كلاسيكية" لبايثون ، في تعريفهم. لديهم القليل من الدقة في أنهم يأخذون وظيفة كمعامل ويعيدون وظيفة.

نعلن أنه يجب تعديل الوظيفة بواسطة مصمم واحد (أو أكثر) بفضل سطر واحد (أو أكثر) فوق تعريف الوظيفة ، مثل هذا:


@nom_du_decorateur
def ma_fonction(...)
 
يعمل المصمم (decorator) عند تحديد الوظيفة ، وليس عند الاتصال. هذا مهم. إنها تأخذ كمعامل ، كما قلت ، وظيفة (التي تعدلها) وتعيد وظيفة (والتي يمكن أن تكون هي نفسها).

انظر بدلاً من ذلك:


>>> def mon_decorateur(fonction):
...     """ أول مثال على الديكور """
...     print("يستدعى مصمم الديكور لدينا بالوظيفة {0}".format(fonction))
...     return fonction
...
>>> @mon_decorateur
... def salut():
...     """ وظيفة تم تعديلها بواسطة مصمم الديكور لدينا """
...     print("مرحبا !")
...
يسمى مصمم الديكور لدينا بالوظيفة 
>>>
 
اه ... ماذا فعلنا هنا؟

  • أولاً ، نصنع المصمم. كما أخبرتك ، فإنه يأخذ الوظيفة التي يعدلها كمعامل. في مثالنا ، يعرض هذه الوظيفة فقط ثم يعيدها.
  • ثم نقوم بإنشاء الدالة salut . كما ترى ، نشير قبل التعريف إلى السطر @mon_decorateur، الذي يخبر Python أنه يجب تعديل هذه الوظيفة بواسطة مصمم الديكور الخاص بنا. وظيفتنا مفيدة للغاية: فهي تعرض "مرحبًا! " وهذا كل شيء.
  • في نهاية تعريف وظيفتنا ، يمكننا أن نرى أن المصمم (decorator) يستدعى. إذا نظرت عن كثب إلى السطر المعروض ، فإنك تدرك أنه تم استدعاؤه ، كمعامل ، مع الوظيفة salut التي حددناها للتو.
دعونا نلقي نظرة فاحصة على هيكل مصمم الديكور لدينا. يتطلب الأمر تعديل الوظيفة كمعامل (المحدد تحت سطر @ ) ، أعتقد أنك رأيته. لكنه يعيد هذه الوظيفة أيضًا وهذا أقل وضوحًا قليلاً!

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

في الوقت الحالي ، تذكر أن الرمزين أدناه متطابقان:


# مثال مع الديكور
@decorateur
def fonction(...):
    ...
# مثال معادل ، بدون ديكور
def fonction(...):
    ...

fonction = decorateur(fonction)
 
اقرأ هذين الرمزين بعناية ، فهما يفعلان نفس الشيء. والثاني متاح لك لفهم ما تفعله Python عندما تعالج الوظائف التي تم تعديلها بواسطة مصمم (أو أكثر) .

عندما تستدعي salut، لا ترى أي تغيير. وهذا أمر طبيعي لأننا نعيد نفس الدالة. المرة الوحيدة التي يتم فيها استدعاء مصمم الديكور لدينا هي عند تحديد وظيفتنا.  وظيفتنا salut لم يتم تعديلها من قبل الديكور لدينا، ونحن راضون باعادتها كما هي.

تعديل سلوك وظيفتنا


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

كيف نعدل سلوك وظيفتنا؟

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

ولكن بعد ذلك ... هل علينا تحديد وظيفة أخرى؟

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

أنا ضائع. كيف تعمل بشكل ملموس؟

سأضع الكود لك ، سيكون أفضل من الكثير من التفسيرات. أعلق عليها أقل قليلاً ، لا تقلق:


def mon_decorateur(fonction):
    """ مصمم الديكور لدينا: سيعرض رسالة قبل الاتصال بـ
     وظيفة محددة """
    
    def fonction_modifiee():
        """الوظيفة التي سنعود إليها. هذا في الواقع نسخة
         تم تعديلها بشكل طفيف من وظيفتنا المحددة في الأصل. 
         يسعدني عرض تحذير قبل تشغيل وظيفتنا
         محددة في الأصل"""
        
        print("حذاري ! نستدعي {0} ".format(fonction))
        return fonction()
    return fonction_modifiee

@mon_decorateur
def salut():
    print("Salut !")
 
دعونا نرى التأثير قبل التفسيرات. لا تظهر أي رسالة عند تشغيل هذا الكود. من ناحية أخرى ، إذا قمت بتنفيذ وظيفتك salut :

>>> salut()
Attention ! On appelle 
Salut !
>>>
 
وإذا عرضت الوظيفة salut في المترجم الفوري ، فستحصل على شيء يثير الدهشة:

>>> salut
<function fonction_modifiee at 0x00BA54B0>
>>>
 
لفهم ذلك ، دعنا نعود إلى كود مصمم الديكور لدينا:
  • كما هو الحال دائمًا ، فإنه يأخذ وظيفة كمعامل. هذه الوظيفة ، عندما نطلب من مصمم الديكور أعلاه def salut، هي salut (الوظيفة المحددة في الأصل) .
  • في جسم مصمم الديكور لدينا ، يمكنك أن ترى أننا حددنا وظيفة جديدة fonction_modifiee،. إنها لا تأخذ أي معايير ، فهي لا تحتاجها. في جسمه ، نعرض خط تحذير بأننا سنقوم بتنفيذ الوظيفة fonction (مرة أخرى ، إنها كذلك salut) . في السطر التالي ، نقوم بتنفيذه فعليًا وإرجاع نتيجة تنفيذه (في حالة salutعدم وجود أي شيء ، ولكن يمكن للوظائف الأخرى إرجاع المعلومات)
  • مرة أخرى في الديكور لدينا ، نشير إلى أننا يجب أن نعود fonction_modifiee .
عند تحديد وظيفتنا salut، نسمي مصمم الديكور. تقوم بايثون بتمرير الوظيفة كمعامل salut . هذه المرة مصمم الديكور لدينا لا يعيد salut إلا fonction_modifiee . ولدينا وظيفة salut، والتي حددناها فقط، لذلك سوف يحل محله وظيفتنا fonction_modifiee، الذي يعرف في الديكور لدينا.

بالمناسبة ، يمكنك رؤيته: عندما تحاول العرض salut في المترجم ، تحصل fonction_modifiee .

تذكر أن الكود:


@mon_decorateur
def salut():
    ...
 
هو نفسه ، بالنسبة لبايثون ، مثل الكود:

def salut():
    ...

salut = mon_decorateur(salut)
 
قد لا يكون أوضح. خذ الوقت الكافي لقراءة وفهم المثال بشكل كامل. الأمر ليس بسيطًا ، فالمنطق موجود بالفعل ولكن عليك قضاء بعض الوقت في الاختبار قبل دمج هذه الفكرة بالكامل.

باختصار ، يُرجع مصمم الديكور لدينا وظيفة الاستبدال. عندما نتصل salut ، فإننا نستدعي وظيفتنا المعدلة والتي تستدعي أيضًا salut بعد عرض رسالة تحذير صغيرة.

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


def obsolete(fonction_origine):
    """ يرمي الزخرف استثناءً ليشير إلى أن الأصل_وظيفة
     عفا عليه الزمن """
    
    def fonction_modifiee():
        raise RuntimeError("وظيفة {0} قديمة!".format(fonction_origine))
    return fonction_modifiee
 
مرة أخرى ، قم بإجراء بعض الاختبارات: سيصبح كل شيء واضحًا بعد بعض التلاعبات.

مصمم مع معلمات


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

وبالتالي ، فإن السطر الذي يطلق على مصمم الديكور الخاص بنا ، فوق تعريف وظيفتنا ، سيكون بالشكل:


@controler_temps(2.5) # 2.5 ثانية كحد أقصى للوظيفة أدناه
 
حتى الآن ، لم يتضمن مصممونا أي قوس بعد دعوتهم. هذان القوسان مهمان للغاية: ستأخذ وظيفة الزخرفة الخاصة بنا المعلمات وليس وظيفة ، ولكن معلمات الديكور (هنا ، الحد الأقصى للوقت المسموح به للوظيفة). لن يعيد وظيفة الاستبدال ، ولكن المصمم.

مرة أخرى وخسر دائما. لماذا يعتبر تمرير المعلمات إلى مصمم الديكور لدينا معقدًا للغاية؟

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


@decorateur
def fonction(...):
    ...
def fonction(...):
    ...

fonction = decorateur(fonction)
 
هذا هو السطر الأخير من المثال الثاني الذي تحتاج إلى تذكره ومحاولة فهمه fonction = decorateur(fonction) .

نستبدل الوظيفة التي حددناها أعلاه بالوظيفة التي أعادها مصمم الديكور لدينا.

هذه هي الآلية الكامنة وراءنا @decorateur .

الآن ، إذا كان مصمم الديكور الخاص بنا يتوقع معلمات ، فسننتهي بسطر مثل هذا:


@decorateur(parametre)
def fonction(...):
    ...
 
وإذا فهمت المثال أعلاه ، فإن هذا الكود هو نفسه:

def fonction(...):
    ...

fonction = decorateur(parametre)(fonction)
 
لقد حذرتك ، إنه ليس بديهيًا جدًا! لكن أعد قراءة هذه الأمثلة بعناية ، يجب أن تأتي النقرة عاجلاً أم آجلاً.

كما ترون ، يجب أن نحدد كمصمم ديكور وظيفة تأخذ معلمات المصمم (decorator) كحجج (هنا ، الوقت المتوقع) والتي ترجع مصمم الديكور. بعبارة أخرى ، نجد أنفسنا مرة أخرى بمستوى إضافي في وظيفتنا.

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


""" لإدارة الوقت ، نقوم باستيراد وحدة الوقت
من هذه الوحدة التي تُرجع عدد time()سنستخدم الوظيفة بشكل أساسي 
الثواني منذ 1 يناير 1970 (عادة).
سنستخدمها لحساب الوقت الذي تستغرقه وظيفتنا في
التنفيذ"""

import time

def controler_temps(nb_secs):
    """ يتحكم في الوقت المستغرق لتشغيل دالة.
    ، فسيتم عرض تنبيه nb_secs إذا كان وقت التنفيذ أكبر من """
    
    def decorateur(fonction_a_executer):
        """ مصمم الديكور لدينا. هو الذي يُدعى مباشرة بعد ذلك
         تعريف وظيفتنا (function_a_executer)"""
        
        def fonction_modifiee():
            """ تم إرجاع الوظيفة بواسطة مصمم الديكور لدينا. هي تتهم
             احسب الوقت الذي تستغرقه الوظيفة في التنفيذ """
            
            tps_avant = time.time() # قبل تنفيذ الوظيفة
            valeur_renvoyee = fonction_a_executer() # ننفذ الوظيفة
            tps_apres = time.time()
            tps_execution = tps_apres - tps_avant
            if tps_execution >= nb_secs:
                print("La fonction {0} a mis {1} pour s'exécuter".format( \
                        fonction_a_executer, tps_execution))
            return valeur_renvoyee
        return fonction_modifiee
    return decorateur
 
آه! ثلاثة مستويات في وظيفتنا! أولاً controler_temps، من يعرّف مصمم الديكور لدينا في جسمه decorateur، والذي هو نفسه يحدد وظيفتنا المعدلة في جسمه fonction_modifiee .

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

يمكننا الآن استخدام مصمم الديكور لدينا. لقد قمت بعمل وظيفة صغيرة لاختبار عرض رسالة إذا كانت وظيفتنا تستغرق وقتًا طويلاً للتنفيذ. انظر بدلاً من ذلك:


>>> @controler_temps(4)
... def attendre():
...     input("Appuyez sur Entrée...")
...
>>> attendre() # Je vais appuyer sur Entrée presque tout de suite
Appuyez sur Entrée...
>>> attendre() # Cette fois, j'attends plus longtemps
Appuyez sur Entrée...
La fonction  a mis 4.14100003242 pour s'exécuter
>>>
 
إنها تعمل ! وحتى إذا كان عليك قضاء بعض الوقت على مصمم الديكور الخاص بك ، نظرًا لمستوياته المختلفة ، عليك أن تعترف أنه سهل الاستخدام للغاية.

لا يزال من الأسهل كتابة:


@controler_temps(4)
def attendre(...)
    ...
 
ان :

def attendre(...):
    ...

attendre = controler_temps(4)(attendre)
 

ضع في الاعتبار معلمات وظيفتنا


حتى الآن ، عملنا فقط مع الوظائف التي لا تأخذ أي معلمات. هذا هو السبب في أن وظيفتنا fonction_modifieeلم تأخذ أي منهما.

نعم ولكن ... ضع في الاعتبار المعلمات ، يمكن أن تكون مفيدةً. خلاف ذلك ، يمكننا فقط بناء ديكورات تنطبق على الوظائف بدون معلمات.

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

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

مرة أخرى ، أعطيك الكود المعدل لوظيفتنا المعدلة. تذكر أنه مُعرَّف في منطقتنا decorateur، وهو نفسه مُعرَّف في controler_temps ((أنا فقط أعطيك كودًا لـ fonction_modifiee ) .


...
        def fonction_modifiee(*parametres_non_nommes, **parametres_nommes):
            """Fonction renvoyée par notre décorateur. Elle se charge
            de calculer le temps mis par la fonction à s'exécuter"""
            
            tps_avant = time.time() # avant d'exécuter la fonction
            ret = fonction_a_executer(*parametres_non_nommes, **parametres_nommes)
            tps_apres = time.time()
            tps_execution = tps_apres - tps_avant
            if tps_execution >= nb_secs:
                print("La fonction {0} a mis {1} pour s'exécuter".format( \
                        fonction_a_executer, tps_execution))
            return ret
 
يمكنك الآن تطبيق هذا المصمم (decorator) على الوظائف التي لا تأخذ أي معلمات ، أو تأخذ رقمًا ، باسم أو لا. ملائم ، أليس كذلك؟

المصممون (decorators) يطبقون على تعريفات الفئة


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


>>> def decorateur(classe):
...     print("Définition de la classe {0}".format(classe))
...     return classe
...
>>> @decorateur
... class Test:
...     pass
...
Définition de la classe <class '__main__.Test'>
>>>
 
هنا. سترى في القسم التالي ما قد يكون من مصلحة التلاعب بتعريفات فئتنا من خلال مصممي الديكور. هناك أمثلة أخرى إلى جانب المثال الذي سأعرضه عليكم بالطبع.

سلسلة الديكور لدينا


يمكنك تعديل وظيفة أو تعريف فئة من خلال أدوات تزيين متعددة ، على شكل


@decorateur1
@decorateur2
def fonction():
 
الأمر ليس أكثر تعقيدًا مما فعلته للتو. أنا أعرضها عليك حتى لا يكون هناك شك في عقلك ، يمكنك اختبار هذا الاحتمال في وقت فراغك ، لنفسك.

سأقدم لك الآن بعض التطبيقات الممكنة لمصممي الديكور ، المستوحاة إلى حد كبير من PEP 318  .

أمثلة على التطبيقات



سنرى مثالين لتطبيقات الديكور في هذا القسم. لقد رأيت أيضًا القليل في القسم السابق ، ولكن الآن بعد أن أتقنت بناء الجملة ، دعنا نتعمق في بعض الأمثلة الأكثر دلالة!

الأقسام singleton


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

بمعنى آخر ، يمكننا فقط إنشاء كائن واحد من هذه الفئة.

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

من السهل جدًا تصميم هذا بفضل المصممين.

كود المثال


def singleton(classe_definie):
    instances = {} # Dictionnaire de nos instances singletons
    def get_instance():
        if classe_definie not in instances:
            # On crée notre premier objet de classe_definie
            instances[classe_definie] = classe_definie()
        return instances[classe_definie]
    return get_instance
 

تفسيرات


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


>>> @singleton
... class Test:
...     pass
...
>>> a = Test()
>>> b = Test()
>>> a is b
True
>>>
 
عندما نقوم بإنشاء كائننا الأول (الكائن الموجود في a ) ، يتم استدعاء المُنشئ الخاص بنا. عندما نريد إنشاء كائن ثانٍ ، a يتم إرجاع الكائن الموجود فيه . لذلك، a و b يشيران إلى نفس الكائن.

الآن دعونا نلقي نظرة على مصمم الديكور لدينا. يعرّف القاموس في جسده. يحتوي هذا القاموس على الفئة كمفتاح singleton والكائن المُنشأ المقابل كقيمة. إنها تعيد وظيفتنا الداخلية get_instance التي ستحل محل صفنا. لذلك ، عندما نريد إنشاء كائن جديد ، فسيتم get_instance استدعاؤه. تتحقق هذه الوظيفة من وجود فئتنا في القاموس. إذا لم يكن الأمر كذلك ، نقوم بإنشاء أول كائن مناظر وإدخاله في القاموس. في جميع الحالات ، نعيد الكائن المقابل إلى القاموس (إما أنه قد تم إنشاؤه للتو ، أو أنه كائن تم إنشاؤه في المرة الأولى التي يتم فيها استدعاء المُنشئ).

بفضل هذا النظام ، يمكننا الإعلان عن عدة فئات على أنها singleton ونحن على يقين من أنه ، لكل فئة من هذه الفئات ، سيتم إنشاء كائن واحد فقط .

التحكم في الأنواع التي تم تمريرها إلى وظيفتنا


لقد لاحظتها بالفعل في Python : لا يوجد تحكم في نوع البيانات التي تم تمريرها كمعلمات لوظائفنا. البعض ، مثل print ، يقبل أي نوع. يطرح البعض الآخر استثناءات عندما يتم توفير معلمة من النوع الخطأ لهم.

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

إليك تعريف وظيفتنا ، لإعطائك فكرة:


@controler_types(int, int)
def intervalle(base_inf, base_sup):
 
controler_types يجب أن يضمن مصمم الديكور لدينا أنه في كل مرة نطلق فيها على الوظيفة intervalle، يتم تمرير الأعداد الصحيحة في المعلمات كـ base_inf و base_sup .

هذا المصمم (decorator) أكثر تعقيدًا ، على الرغم من أنني قمت بتبسيط مثال PEP 318 قدر الإمكان.

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

كود المثال


def controler_types(*a_args, **a_kwargs):
    """الأنواع المرغوبة متوقعة في معلمات الديكور. نحن نقبل
     قائمة المعلمات غير المحددة ، بالنظر إلى أن وظيفتنا
     يمكن استدعاء المحدد مع عدد متغير من المعلمات ولذاك
     يجب السيطرة على الجميع"""
    
    def decorateur(fonction_a_executer):
        """ مصمم الديكور لدينا. يجب أن ترجع دالة معدلة """
        def fonction_modifiee(*args, **kwargs):
            """ وظيفتنا المعدلة. هي المسؤولة عن التحكم
             الأنواع التي يتم تمريرها إليها في المعلمات """
            
            # يجب أن تكون قائمة المعلمات المتوقعة (a_args) هي نفسها
            # )args الطول كما تم استلامه (
            if len(a_args) != len(args):
                raise TypeError("العدد المتوقع من الوسائط غير متساوي " \
                        " إلى العدد المستلم ")
            # نراجع قائمة الحجج المستلمة وغير المسماة
            for i, arg in enumerate(args):
                if a_args[i] is not type(args[i]):
                    raise TypeError("l'argument {0} n'est pas du type " \
                            "{1}".format(i, a_args[i]))
            
            # ننتقل الآن إلى قائمة المعلمات المستلمة والمسماة
            for cle in kwargs:
                if cle not in a_kwargs:
                    raise TypeError("l'argument {0} n'a aucun type " \
                            "précisé".format(repr(cle)))
                if a_kwargs[cle] is not type(kwargs[cle]):
                    raise TypeError("l'argument {0} n'est pas de type" \
                            "{1}".format(repr(cle), a_kwargs[cle]))
            return fonction_a_executer(*args, **kwargs)
        return fonction_modifiee
    return decorateur
 

تفسيرات


إنه مصمم ديكور معقد جدًا (ومع ذلك صدقوني لقد قمت بتبسيطه قدر الإمكان). سنرى أولاً كيفية استخدامه:


>>> @controler_types(int, int)
... def intervalle(base_inf, base_sup):
...     print("{1} تتراوح من {0} إلى ".format(base_inf, base_sup))
...
>>> intervalle(1, 8)
Intervalle de 1 à 8
>>> intervalle(5, "oups!")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 24, in fonction_modifiee
TypeError: l'argument 1 n'est pas du type <class 'int'>
>>>
 
هنا مرة أخرى ، الاستخدام بسيط للغاية. دعونا نلقي نظرة على المصمم (decorator) نفسه ، إنه بالفعل أكثر تعقيدًا.

يجب أن يأخذ مصمم الديكور لدينا المعلمات (قائمة بالمعلمات غير المحددة بالمناسبة ، لأن وظيفتنا يجب أن تأخذ أيضًا قائمة من المعلمات غير المحددة ويجب علينا التحكم في كل منها). لذلك نحدد معلمة a_args تحتوي على قائمة بأنواع المعلمات غير المسماة المتوقعة ، ومعلمة ثانية a_kwargs تحتوي على قاموس أنواع المعلمات المسماة المتوقعة.

هل مازلت تتابع؟

يجب أن تفهم البناء العام ، لقد رأينا ذلك قليلاً أعلاه. له ثلاثة مستويات ، حيث يتعين علينا التأثير على سلوك الوظيفة ويأخذ مصمم الديكور لدينا المعلمات. يوجد كود التحكم الخاص بنا ، كما ينبغي ، في وظيفتنا fonction_modifiee (والتي ستحل محلنا fonction_a_executer ) .

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

إذا سارت الأمور على ما يرام (لم يتم طرح أي استثناء) ، فإننا ننفذ وظيفتنا ، ونعيد نتيجتها.

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

باختصار


  • يسمح لك المصممون (decorators) بتعديل سلوك الوظيفة.
  • هم أنفسهم وظائف ، يأخذون وظيفة كمعامل ويعيدون وظيفة (والتي يمكن أن تكون هي نفسها).
  • يمكنك التصريح عن دالة على أنها مزخرفة بوضع السطر فوق خط تعريفها @nom_decorateur .
  • عند تحديد الوظيفة ، يتم استدعاء المصمم (decorator) وستكون الوظيفة التي يتم إرجاعها هي الوظيفة المستخدمة.
  • يمكن للمصممين أيضًا أخذ المعلمات للتأثير على سلوك الميزة المزخرفة.