Python: البرمجة التخطيطية في الإنتاج. الجزء الثاني

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



الآن دعنا نرى كيف يمكنك تغيير مكالمات الطريقة. يمكنك معرفة المزيد حول خيارات البرمجة التخطيطية في دورة Advanced Python .


تصحيح أخطاء المكالمات وتتبعها


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


يقيس metaclass التالي وقت تنفيذ كل طريقة في الفصل ومثيلاتها ، بالإضافة إلى وقت إنشاء المثيل نفسه:


from contextlib import contextmanager import logging import time import wrapt @contextmanager def timing_context(operation_name): """       """ start_time = time.time() try: yield finally: logging.info('Operation "%s" completed in %0.2f seconds', operation_name, time.time() - start_time) @wrapt.decorator def timing(func, instance, args, kwargs): """       .     https://wrapt.readthedocs.io/en/latest/         """ with timing_context(func.__name__): return func(*args, **kwargs) class DebugMeta(type): def __new__(mcs, name, bases, attrs): for attr, method in attrs.items(): if not attr.startswith('_'): #     attrs[attr] = timing(method) return super().__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): with timing_context(f'{cls.__name__} instance creation'): #      return super().__call__(*args, **kwargs) 

دعونا نلقي نظرة على التصحيح في العمل:


 class User(metaclass=DebugMeta): def __init__(self, name): self.name = name time.sleep(.7) def login(self): time.sleep(1) def logout(self): time.sleep(2) @classmethod def create(cls): time.sleep(.5) user = User('Michael') user.login() user.logout() user.create() #   INFO:__main__:Operation "User instance creation" completed in 0.70 seconds INFO:__main__:Operation "login" completed in 1.00 seconds INFO:__main__:Operation "logout" completed in 2.00 seconds INFO:__main__:Operation "create" completed in 0.50 seconds 

جرب توسيع DebugMeta وتسجيل توقيع الأساليب DebugMeta .


نمط وحيد وحظر الميراث


والآن دعنا ننتقل إلى الحالات الغريبة لاستخدام metaclass في مشاريع Python.


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


 class Singleton(type): instance = None def __call__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__call__(*args, **kwargs) return cls.instance class User(metaclass=Singleton): def __init__(self, name): self.name = name def __repr__(self): return f'<User: {self.name}>' u1 = User('Pavel') #         u2 = User('Stepan') >>> id(u1) == id(u2) True >>> u2 <User: Pavel> >>> User.instance <User: Pavel> #   , ? >>> u1.instance.instance.instance.instance <User: Pavel> 

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


 >>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66) #     ! <User: Roman> #     User       #    TypeError! 

الآن دعونا نلقي نظرة على خيار أكثر غرابة: حظر الميراث من فئة معينة.


 class FinalMeta(type): def __new__(mcs, name, bases, attrs): for cls in bases: if isinstance(cls, FinalMeta): raise TypeError(f"Can't inherit {name} class from final {cls.__name__}") return super().__new__(mcs, name, bases, attrs) class A(metaclass=FinalMeta): """   !""" pass class B(A): pass # TypeError: Can't inherit B class from final A #    ! 

معلمة Metaclass


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


على سبيل المثال ، يمكنك تمرير دالة إلى معلمة metaclass عند الإعلان عن فئة وإرجاع مثيلات مختلفة من metaclass اعتمادًا على بعض الشروط ، على سبيل المثال:


 def get_meta(name, bases, attrs): if SOME_SETTING: return MetaClass1(name, bases, attrs) else: return MetaClass2(name, bases, attrs) class A(metaclass=get_meta): pass 

لكن المثال الأكثر إثارة للاهتمام هو استخدام معلمات extra_kwargs عند الإعلان عن الفئات. افترض أنك تريد استخدام metaclass لتغيير سلوك طرق معينة في الفصل الدراسي ، وقد يكون لكل فئة أسماء مختلفة لهذه الأساليب. ماذا تفعل؟ وهنا ما


 #   `DebugMeta`     class DebugMetaParametrized(type): def __new__(mcs, name, bases, attrs, **extra_kwargs): debug_methods = extra_kwargs.get('debug_methods', ()) for attr, value in attrs.items(): #      ,   #    `debug_methods`: if attr in debug_methods: attrs[attr] = timing(value) return super().__new__(mcs, name, bases, attrs) class User(metaclass=DebugMetaParametrized, debug_methods=('login', 'create')): ... user = User('Oleg') user.login() #  "logout"   . user.logout() user.create() 

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


__prepare__ الطريقة


أخيرًا ، سأتحدث عن إمكانية استخدام طريقة __prepare__ . كما ذكر أعلاه ، يجب أن ترجع هذه الطريقة كائن القاموس ، الذي __prepare__ المترجم في لحظة تحليل نص الفئة ، على سبيل المثال ، إذا كان __prepare__ يُرجع الكائن d = dict() ، فعندئذ عند قراءة الفصل التالي:


 class A: x = 12 y = 'abc' z = {1: 2} 

يقوم المترجم بالعمليات التالية:


 d['x'] = 12 d['y'] = 'abc' d['z'] = {1: 2} 

هناك العديد من الاستخدامات الممكنة لهذه الميزة. إنها جميعها بدرجات متفاوتة من الفائدة ، لذلك:


  1. في إصدارات Python = <3.5 ، إذا احتجنا إلى الحفاظ على ترتيب التصريح في الصف ، فيمكننا إرجاع collections.OrderedDict من طريقة __prepare__ ، في الإصدارات الأقدم ، تحتفظ القواميس المضمنة بالفعل بترتيب إضافة المفاتيح ، لذلك لم تعد هناك حاجة إلى OrderedDict .
  2. تستخدم وحدة المكتبة القياسية التعدادية كائنًا شبيهًا بالإملاء لتحديد متى يتم تكرار سمة فئة عند الإعلان. يمكن العثور على الرمز هنا .
  3. ليس رمزًا جاهزًا للإنتاج على الإطلاق ، ولكن من الأمثلة الجيدة جدًا دعم تعدد الأشكال المعياري .

على سبيل المثال ، ضع في اعتبارك الفئة التالية مع ثلاثة تطبيقات لطريقة متعددة الأشكال:


 class Terminator: def terminate(self, x: int): print(f'Terminating INTEGER {x}') def terminate(self, x: str): print(f'Terminating STRING {x}') def terminate(self, x: dict): print(f'Terminating DICTIONARY {x}') t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'}) #  Terminating DICTIONARY 10 Terminating DICTIONARY Hello, world! Terminating DICTIONARY {'hello': 'world'} 

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


 class PolyDict(dict): """ ,               PolyMethod. """ def __setitem__(self, key: str, func): if not key.startswith('_'): if key not in self: super().__setitem__(key, PolyMethod()) self[key].add_implementation(func) return None return super().__setitem__(key, func) class PolyMethod: """    ,            .       ,       : instance method, staticmethod, classmethod. """ def __init__(self): self.implementations = {} self.instance = None self.cls = None def __get__(self, instance, cls): self.instance = instance self.cls = cls return self def _get_callable_func(self, impl): # ""  classmethod/staticmethod return getattr(impl, '__func__', impl) def __call__(self, arg): impl = self.implementations[type(arg)] callable_func = self._get_callable_func(impl) if isinstance(impl, staticmethod): return callable_func(arg) elif self.cls and isinstance(impl, classmethod): return callable_func(self.cls, arg) else: return callable_func(self.instance, arg) def add_implementation(self, func): callable_func = self._get_callable_func(func) #   ,     1  arg_name, arg_type = list(callable_func.__annotations__.items())[0] self.implementations[arg_type] = func 

الشيء الأكثر إثارة للاهتمام في الشفرة أعلاه هو كائن PolyMethod ، الذي يخزن PolyMethod لنفس الطريقة ، اعتمادًا على نوع الوسيطة التي تم تمريرها إلى هذه الطريقة. سنقوم بإرجاع كائن PolyDict من طريقة __prepare__ وبالتالي __prepare__ التنفيذ المختلفة للطرق التي terminate بنفس الاسم. نقطة مهمة - عند قراءة نص الفصل وعند إنشاء كائن attrs ، يضع المترجم ما يسمى بالوظائف unbound هناك ، ولا تعرف هذه الدالات بعد الفئة أو الحالة التي سيتم استدعاؤها. كان علينا تنفيذ بروتوكول واصف من أجل تحديد السياق أثناء استدعاء الوظيفة وتمرير إما self أو cls كمعلمة أولى ، أو تمرير أي شيء إذا staticmethod استدعاء staticmethod .


ونتيجة لذلك ، سنرى السحر التالي:


 class PolyMeta(type): @classmethod def __prepare__(mcs, name, bases): return PolyDict() class Terminator(metaclass=PolyMeta): ... t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'}) #  Terminating INTEGER 10 Terminating STRING Hello, world! Terminating DICTIONARY {'hello': 'world'} >>> t1000.terminate <__main__.PolyMethod object at 0xdeadcafe> 

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


الخلاصة


يعد Metaprogramming أحد الموضوعات العديدة التي تحدثت عنها في Advanced Python . كجزء من الدورة ، سأخبرك أيضًا بكيفية استخدام مبادئ SOLID و GRASP بشكل فعال في تطوير مشاريع Python الكبيرة ، وتصميم بنية التطبيق وكتابة كود عالي الأداء وجودة عالية. سأكون سعيدًا برؤيتك في جدران حي ثنائي!

Source: https://habr.com/ru/post/ar422415/


All Articles