نواصل الحديث عن 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('_'):
دعونا نلقي نظرة على التصحيح في العمل:
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()
جرب توسيع 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')
هذا التطبيق له فارق بسيط مثير للاهتمام - حيث لا يتم استدعاء مُنشئ الصف للمرة الثانية ، يمكنك ارتكاب خطأ وعدم تمرير المعلمة الضرورية هناك ، ولن يحدث أي شيء في وقت التشغيل إذا تم إنشاء المثيل بالفعل. على سبيل المثال:
>>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66)
الآن دعونا نلقي نظرة على خيار أكثر غرابة: حظر الميراث من فئة معينة.
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
في الأمثلة السابقة ، استخدمنا 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 لتغيير سلوك طرق معينة في الفصل الدراسي ، وقد يكون لكل فئة أسماء مختلفة لهذه الأساليب. ماذا تفعل؟ وهنا ما
في رأيي ، تبين أنها أنيقة للغاية! يمكنك التوصل إلى الكثير من الأنماط لاستخدام هذه المعلمة ، ولكن تذكر القاعدة الرئيسية - كل شيء جيد في الاعتدال.
__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}
هناك العديد من الاستخدامات الممكنة لهذه الميزة. إنها جميعها بدرجات متفاوتة من الفائدة ، لذلك:
- في إصدارات Python = <3.5 ، إذا احتجنا إلى الحفاظ على ترتيب التصريح في الصف ، فيمكننا إرجاع
collections.OrderedDict
من طريقة __prepare__
، في الإصدارات الأقدم ، تحتفظ القواميس المضمنة بالفعل بترتيب إضافة المفاتيح ، لذلك لم تعد هناك حاجة إلى OrderedDict
. - تستخدم وحدة المكتبة القياسية التعدادية كائنًا شبيهًا بالإملاء لتحديد متى يتم تكرار سمة فئة عند الإعلان. يمكن العثور على الرمز هنا .
- ليس رمزًا جاهزًا للإنتاج على الإطلاق ، ولكن من الأمثلة الجيدة جدًا دعم تعدد الأشكال المعياري .
على سبيل المثال ، ضع في اعتبارك الفئة التالية مع ثلاثة تطبيقات لطريقة متعددة الأشكال:
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'})
من الواضح أن طريقة 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):
الشيء الأكثر إثارة للاهتمام في الشفرة أعلاه هو كائن 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'})
إذا كنت تعرف أي استخدامات أخرى مثيرة للاهتمام لطريقة __prepare__
، فيرجى كتابة التعليقات.
الخلاصة
يعد Metaprogramming أحد الموضوعات العديدة التي تحدثت عنها في Advanced Python . كجزء من الدورة ، سأخبرك أيضًا بكيفية استخدام مبادئ SOLID و GRASP بشكل فعال في تطوير مشاريع Python الكبيرة ، وتصميم بنية التطبيق وكتابة كود عالي الأداء وجودة عالية. سأكون سعيدًا برؤيتك في جدران حي ثنائي!