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

يعتقد الكثير من الناس أن البرمجة التخطيطية في Python تعقد التعليمات البرمجية بشكل غير ضروري ، ولكن إذا كنت تستخدمها بشكل صحيح ، يمكنك تنفيذ أنماط التصميم المعقدة بشكل سريع وأنيق. بالإضافة إلى ذلك ، تستخدم أطر عمل Python المعروفة مثل Django و DRF و SQLAlchemy metaclasses لتوفير سهولة التوسعة وإعادة استخدام التعليمات البرمجية بسهولة.



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


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


جولة سريعة في نموذج بيانات Python


لذلك ، نعلم جميعًا أن كل شيء في Python هو كائن ، وليس سراً أنه لكل كائن هناك فئة معينة تم إنشاؤها من خلالها ، على سبيل المثال:


>>> def f(): pass >>> type(f) <class 'function'> 

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


لذلك ، لإنشاء وظائف ، function فئة معينة مضمنة. دعونا نرى ما يمكننا القيام به. للقيام بذلك ، خذ الفراغ من وحدة الأنواع المدمجة:


 >>> from types import FunctionType >>> FunctionType <class 'function'> >>> help(FunctionType) class function(object) | function(code, globals[, name[, argdefs[, closure]]]) | | Create a function object from a code object and a dictionary. | The optional name string overrides the name from the code object. | The optional argdefs tuple specifies the default argument values. | The optional closure tuple supplies the bindings for free variables. 

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


 #   ,    "Hello, world!" >>> code = compile('print("Hello, world!")', '<repl>', 'eval') >>> code <code object <module> at 0xdeadbeef, file "<repl>", line 1> #  ,     , #      >>> func = FunctionType(code, globals(), 'greetings') >>> func <function <module> at 0xcafefeed> >>> func.__name__ 'greetings' >>> func() Hello, world! 

عظيم! بمساعدة الأدوات الفوقية ، تعلمنا كيفية إنشاء وظائف على الطاير ، ولكن في الواقع نادرًا ما يتم استخدام هذه المعرفة. الآن دعونا نلقي نظرة على كيفية إنشاء كائنات الفئة وكائنات مثيل هذه الفئات:


 >>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'> 

من الواضح تمامًا أن فئة المستخدم تُستخدم لإنشاء مثيل user ، ومن المثير للاهتمام كثيرًا أن ننظر إلى فئة type ، والتي تُستخدم لإنشاء فئة User نفسها. هنا سوف ننتقل إلى الخيار الثاني لاستدعاء وظيفة type المضمنة ، والتي تكون في مجملها طبقة metaclass لأي فئة في Python. طبقة metaclass ، بحكم تعريفها ، هي فئة مثيلها فئة أخرى. تتيح لنا Metaclasses تخصيص عملية إنشاء فئة والتحكم جزئيًا في عملية إنشاء مثيل لفئة.


وفقًا للتوثيق ، فإن type(name, bases, attrs) التوقيع الثاني type(name, bases, attrs) - يُرجع نوع بيانات جديد أو ، إذا كان بطريقة بسيطة - فئة جديدة ، وتصبح سمة name سمة __name__ للفئة التي تم إرجاعها ، bases - ستتوفر قائمة فئات الأصل على أنها __bases__ ، حسنًا ، attrs - كائن يشبه __dict__ يحتوي على جميع سمات وأساليب الفصل ، سوف يدخل في __dict__ . يمكن وصف مبدأ الوظيفة على أنها كود زائف بسيط في Python:


 type(name, bases, attrs) ~ class name(bases): attrs 

دعونا نرى كيف يمكنك ، باستخدام type الاتصال فقط ، إنشاء فئة جديدة تمامًا:


 >>> User = type('User', (), {}) >>> User <class '__main__.User'> 

كما ترى ، نحن لسنا بحاجة إلى استخدام الكلمة الأساسية class لإنشاء فئة جديدة ، وظيفة type بدونها ، دعنا الآن ننظر إلى مثال أكثر تعقيدًا:


 class User: def __init__(self, name): self.name = name class SuperUser(User): """Encapsulate domain logic to work with super users""" group_name = 'admin' @property def login(self): return f'{self.group_name}/{self.name}'.lower() #     SuperUser "" CustomSuperUser = type( #   'SuperUser', #  ,      (User, ), #         { '__doc__': 'Encapsulate domain logic to work with super users', 'group_name': 'admin', 'login': property(lambda self: f'{self.group_name}/{self.name}'.lower()), } ) assert SuperUser.__doc__ == CustomSuperUser.__doc__ assert SuperUser('Vladimir').login == CustomSuperUser('Vladimir').login 

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


إنشاء النماذج والمدققين ديناميكيًا


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


للتوضيح ، دعنا نحاول إنشاء نموذج Django ديناميكيًا ، يتم وصف وصف المخطط الذي تم تخزينه بتنسيق json التالي:


 { "fist_name": { "type": "str", "max_length": 25 }, "last_name": { "type": "str", "max_length": 30 }, "age": { "type": "int", "min_value": 18, "max_value": 99 } } 

الآن ، بناءً على الوصف أعلاه ، قم بإنشاء مجموعة من الحقول ونموذج جديد باستخدام دالة type التي نعرفها بالفعل:


 import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, } # form_description –  json    deserialized_form_description: dict = json.loads(form_description) form_attrs = {} #            for field_name, field_description in deserialized_form_description.items(): field_class = fields_type_map[field_description.pop('type')] form_attrs[field_name] = field_class(**field_description) user_form_class = type('DynamicForm', (forms.Form, ), form_attrs) >>> form = user_form_class({'age': 101}) >>> form <DynamicForm bound=True, valid=Unknown, fields=(fist_name;last_name;age)> >>> form.is_valid() False >>> form.errors {'fist_name': ['This field is required.'], 'last_name': ['This field is required.'], 'age': ['Ensure this value is less than or equal to 99.']} 

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


تكوين إنشاء فئة جديدة من خلال metaclass


أعلاه ، نظرنا في type metaclass بالفعل "انتهى" ، ولكن في معظم الأحيان في الكود ستقوم بإنشاء metaclasses الخاصة بك واستخدامها لتكوين إنشاء فئات جديدة وحالاتها. في الحالة العامة ، يبدو "الفراغ" في metaclass كما يلي:


 class MetaClass(type): """   : mcs –  ,  <__main__.MetaClass> name – ,  ,     ,  "User" bases –   -,  (SomeMixin, AbstractUser) attrs – dict-like ,         cls –  ,  <__main__.User> extra_kwargs –  keyword-     args  kwargs –          """ def __new__(mcs, name, bases, attrs, **extra_kwargs): return super().__new__(mcs, name, bases, attrs) def __init__(cls, name, bases, attrs, **extra_kwargs): super().__init__(cls) @classmethod def __prepare__(mcs, cls, bases, **extra_kwargs): return super().__prepare__(mcs, cls, bases, **kwargs) def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs) 

لاستخدام هذه الطبقة الأولية لتكوين فئة User ، يتم استخدام بناء الجملة التالي:


 class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name 

الشيء الأكثر إثارة للاهتمام هو الترتيب الذي يسمي فيه مترجم Python طريقة metaclass metamethod في الوقت الذي يتم فيه إنشاء الفصل نفسه:


  1. يحدد المترجم ويجد الفئات الأصل للفئة الحالية (إن وجدت).
  2. يحدد المترجم metaclass ( MetaClass في حالتنا).
  3. يتم MetaClass.__prepare__ طريقة MetaClass.__prepare__ - يجب أن تقوم بإرجاع كائن يشبه MetaClass.__prepare__ حيث سيتم كتابة سمات وأساليب الفئة. بعد ذلك ، سيتم تمرير الكائن إلى MetaClass.__new__ خلال وسيطة attrs . سنتحدث عن الاستخدام العملي لهذه الطريقة بعد قليل في الأمثلة.
  4. يقرأ المترجم نص فئة User ويولد معلمات MetaClass إلى MetaClass metaclass.
  5. طريقة MetaClass.__new__ - طريقة MetaClass.__new__ ، تقوم بإرجاع كائن الفئة الذي تم إنشاؤه. لقد التقينا بالفعل name الوسيطات bases attrs عند تمريرها إلى دالة type ، وسنتحدث عن المعلمة **extra_kwargs بعد ذلك بقليل. إذا تم تغيير نوع وسيطة attrs باستخدام __prepare__ ، فيجب تحويلها إلى dict قبل تمريرها إلى استدعاء الأسلوب super() .
  6. MetaClass.__init__ طريقة MetaClass.__init__ - طريقة التهيئة التي يمكنك من خلالها إضافة سمات وأساليب إضافية إلى كائن الفئة في الفئة. من الناحية العملية ، يتم استخدامه في الحالات التي يتم فيها توريث metaclasses من metaclasses الأخرى ، وإلا فإن كل ما يمكن القيام به في __init__ يتم بشكل أفضل في __new__ . على سبيل المثال ، لا يمكن تعيين المعلمة __slots__ إلا في طريقة __new__ عن طريق كتابتها إلى كائن attrs .
  7. في هذه الخطوة ، يعتبر الفصل تم إنشاؤه.

الآن قم بإنشاء مثيل من فئة User بنا وإلقاء نظرة على سلسلة الاتصال:


 user = User(name='Alyosha') 

  1. في وقت استدعاء User(...) المترجم باستدعاء طريقة MetaClass.__call__(name='Alyosha') ، حيث يمر كائن الفئة MetaClass.__call__(name='Alyosha') التي تم تمريرها.
  2. MetaClass.__call__ يستدعي User.__new__(name='Alyosha') - طريقة مُنشئ تُنشئ وتُرجع MetaClass.__call__ لفئة User
  3. بعد ذلك ، MetaClass.__call__ يتصل User.__init__(name='Alyosha') - طريقة تهيئة تضيف سمات جديدة إلى المثيل الذي تم إنشاؤه.
  4. MetaClass.__call__ تُرجع MetaClass.__call__ لفئة User .
  5. عند هذه النقطة ، يتم إنشاء مثيل للفئة.

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


دروس مجردة


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


 from abc import ABCMeta, abstractmethod class BasePlugin(metaclass=ABCMeta): """   supported_formats   run        """ @property @abstractmethod def supported_formats(self) -> list: pass @abstractmethod def run(self, input_data: dict): pass 

إذا لم يتم تنفيذ جميع الأساليب والصفات المجردة في الوريث ، فعندما نحاول إنشاء مثيل لفئة الوريث ، نحصل على TypeError :


 class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin() # TypeError: Can't instantiate abstract class VideoPlugin # with abstract methods supported_formats 

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


نظام مساعد التسجيل التلقائي


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


تنفيذ Metaclass:


 class RegistryMeta(ABCMeta): """ ,      .     " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs) #     (BasePlugin) if inspect.isabstract(cls): return cls for media_format in cls.supported_formats: if media_format in mcs._registry_formats: raise ValueError(f'Format {media_format} is already registered') #       mcs._registry_formats[media_format] = cls return cls @classmethod def get_plugin(mcs, media_format: str): try: return mcs._registry_formats[media_format] except KeyError: raise RuntimeError(f'Plugin is not defined for {media_format}') @classmethod def show_registry(mcs): from pprint import pprint pprint(mcs._registry_formats) 

وإليك الإضافات نفسها ، سنأخذ تطبيق BasePlugin من المثال السابق:


 class BasePlugin(metaclass=RegistryMeta): ... class VideoPlugin(BasePlugin): supported_formats = ['mpg', 'mov'] def run(self): ... class AudioPlugin(BasePlugin): supported_formats = ['mp3', 'flac'] def run(self): ... 

بعد تنفيذ هذا الرمز ، سيقوم المترجم بتسجيل 4 تنسيقات ومكوّنين إضافيين في التسجيل لدينا يمكنهم معالجة هذه التنسيقات:


 >>> RegistryMeta.show_registry() {'flac': <class '__main__.AudioPlugin'>, 'mov': <class '__main__.VideoPlugin'>, 'mp3': <class '__main__.AudioPlugin'>, 'mpg': <class '__main__.VideoPlugin'>} >>> plugin_class = RegistryMeta.get_plugin('mov') >>> plugin_class <class '__main__.VideoPlugin'> >>> plugin_class().run() Processing video... 

من الجدير بالذكر فارق واحد أكثر إثارة للاهتمام للعمل مع metaclasses ، وذلك بفضل ترتيب حل الطريقة غير الواضح ، يمكننا استدعاء طريقة show_registry ليس فقط في فئة RegistyMeta ، ولكن في أي فئة أخرى يكون فيها metaclass:


 >>> AudioPlugin.get_plugin('avi') # RuntimeError: Plugin is not found for avi 

استخدام أسماء السمات كبيانات وصفية


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


 class Book(models.Model): title = models.Charfield(max_length=250) 

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


 class Message: def __init__(self, text, code=None): self.text = text self.code = code def to_json(self): return json.dumps({'text': self.text, 'code': self.code}) 

سيتم تخزين جميع رسائل الخطأ الخاصة بنا في "مساحة اسم" منفصلة:


 class Messages: not_found = Message('Resource not found') bad_request = Message('Request body is invalid') ... >>> Messages.not_found.to_json() {"text": "Resource not found", "code": null} 

نريد الآن code لا يصبح code null ، ولكن not_found ، ولهذا نكتب الطبقة الفوقية التالية:


 class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items(): #          Message #    code    # ( code   ) if isinstance(value, Message) and value.code is None: value.code = attr return super().__new__(mcs, name, bases, attrs) class Messages(metaclass=MetaMessage): ... 

دعونا نرى كيف تبدو مشاركاتنا الآن:


 >>> Messages.not_found.to_json() {"text": "Resource not found", "code": "not_found"} >>> Messages.bad_request.to_json() {"text": "Request body is invalid", "code": "bad_request"} 

ما تحتاجه! الآن أنت تعرف ما يجب فعله بحيث يمكنك بسهولة من خلال تنسيق البيانات العثور على الرمز الذي يعالجها.


تخزين البيانات الوصفية للتخزين المؤقت عن الفصل الدراسي ونسله


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


كيف يمكن استخدام هذا؟ افترض أنك تقوم بتطوير إطار عمل لبناء التقارير والجداول ولديك مثل هذا الكائن:


 class Row(metaclass=MetaRow): name: str age: int ... def __init__(self, **kwargs): self.counter = None for attr, value in kwargs.items(): setattr(self, attr, value) def __str__(self): out = [self.counter] #  __header__      for name in self.__header__[1:]: out.append(getattr(self, name, 'N/A')) return ' | '.join(map(str, out)) 

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


 class MetaRow(type): #      row_count = 0 def __new__(mcs, name, bases, attrs): cls = super().__new__(mcs, name, bases, attrs) #          cls.__header__ = ['№'] + sorted(attrs['__annotations__'].keys()) return cls def __call__(cls, *args, **kwargs): #      row: 'Row' = super().__call__(*args, **kwargs) #    cls.row_count += 1 #     row.counter = cls.row_count return row 

يجب توضيح شيئين هنا:


  • لا تحتوي فئة الصف على سمات فئة مع name age - وهذه هي التعليقات التوضيحية ، لذا فهي ليست في مفاتيح قاموس attrs ، __annotations__ على قائمة الحقول ، نستخدم __annotations__ للفئة.
  • كان من المفترض أن cls.row_count += 1 العملية cls.row_count += 1 : كيف ذلك؟ بعد كل شيء ، cls هي فئة Row ؛ لا تحتوي على سمة row_count . كل شيء صحيح ، ولكن كما أوضحت أعلاه - إذا لم يكن للفئة التي تم إنشاؤها سمة أو طريقة يحاولون الاتصال بها ، فإن المترجم يذهب إلى أبعد من ذلك في سلسلة الفئات الأساسية - إذا لم يكن هناك أي منها ، يتم البحث في الطبقة الفوقية. في مثل هذه الحالات ، حتى لا تربك أي شخص ، من الأفضل استخدام سجل آخر: MetaRow.row_count += 1 .

شاهد كيف يمكنك الآن عرض الجدول بأكمله بأناقة:


 rows = [ Row(name='Valentin', age=25), Row(name='Sergey', age=33), Row(name='Gosha'), ] print(' | '.join(Row.__header__)) for row in rows: print(row) 

 | age | name 1 | 25 | Valentin 2 | 33 | Sergey 3 | N/A | Gosha 

بالمناسبة ، يمكن تغليف العرض والعمل مع جدول في فئة Sheet منفصلة.


يتبع ...


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


بمزيد من التفاصيل حول metaclasses والوصف في Python ، سأخبر في إطار Python المتقدم .

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


All Articles