يعتقد الكثير من الناس أن البرمجة التخطيطية في 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
. للقيام بذلك ، نحتاج إلى معرفة كيفية إنشاء كائنات التعليمات البرمجية باستخدام وظيفة الترجمة المضمنة في المترجم:
عظيم! بمساعدة الأدوات الفوقية ، تعلمنا كيفية إنشاء وظائف على الطاير ، ولكن في الواقع نادرًا ما يتم استخدام هذه المعرفة. الآن دعونا نلقي نظرة على كيفية إنشاء كائنات الفئة وكائنات مثيل هذه الفئات:
>>> 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()
كما ترون من الأمثلة أعلاه ، فإن وصف الفئات والوظائف باستخدام 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, }
عظيم! يمكنك الآن نقل النموذج الذي تم إنشاؤه إلى النموذج وتقديمه للمستخدم. يمكن استخدام نفس النهج مع أطر عمل أخرى للتحقق من صحة البيانات وعرضها ( DRF Serializers ، الخطمي وغيرها).
أعلاه ، نظرنا في 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 في الوقت الذي يتم فيه إنشاء الفصل نفسه:
- يحدد المترجم ويجد الفئات الأصل للفئة الحالية (إن وجدت).
- يحدد المترجم metaclass (
MetaClass
في حالتنا). - يتم
MetaClass.__prepare__
طريقة MetaClass.__prepare__
- يجب أن تقوم بإرجاع كائن يشبه MetaClass.__prepare__
حيث سيتم كتابة سمات وأساليب الفئة. بعد ذلك ، سيتم تمرير الكائن إلى MetaClass.__new__
خلال وسيطة attrs
. سنتحدث عن الاستخدام العملي لهذه الطريقة بعد قليل في الأمثلة. - يقرأ المترجم نص فئة
User
ويولد معلمات MetaClass
إلى MetaClass
metaclass. - طريقة
MetaClass.__new__
- طريقة MetaClass.__new__
، تقوم بإرجاع كائن الفئة الذي تم إنشاؤه. لقد التقينا بالفعل name
الوسيطات bases
attrs
عند تمريرها إلى دالة type
، وسنتحدث عن المعلمة **extra_kwargs
بعد ذلك بقليل. إذا تم تغيير نوع وسيطة attrs
باستخدام __prepare__
، فيجب تحويلها إلى dict
قبل تمريرها إلى استدعاء الأسلوب super()
. MetaClass.__init__
طريقة MetaClass.__init__
- طريقة التهيئة التي يمكنك من خلالها إضافة سمات وأساليب إضافية إلى كائن الفئة في الفئة. من الناحية العملية ، يتم استخدامه في الحالات التي يتم فيها توريث metaclasses من metaclasses الأخرى ، وإلا فإن كل ما يمكن القيام به في __init__
يتم بشكل أفضل في __new__
. على سبيل المثال ، لا يمكن تعيين المعلمة __slots__
إلا في طريقة __new__
عن طريق كتابتها إلى كائن attrs
.- في هذه الخطوة ، يعتبر الفصل تم إنشاؤه.
الآن قم بإنشاء مثيل من فئة User
بنا وإلقاء نظرة على سلسلة الاتصال:
user = User(name='Alyosha')
- في وقت استدعاء
User(...)
المترجم باستدعاء طريقة MetaClass.__call__(name='Alyosha')
، حيث يمر كائن الفئة MetaClass.__call__(name='Alyosha')
التي تم تمريرها. MetaClass.__call__
يستدعي User.__new__(name='Alyosha')
- طريقة مُنشئ تُنشئ وتُرجع MetaClass.__call__
لفئة User
- بعد ذلك ،
MetaClass.__call__
يتصل User.__init__(name='Alyosha')
- طريقة تهيئة تضيف سمات جديدة إلى المثيل الذي تم إنشاؤه. MetaClass.__call__
تُرجع MetaClass.__call__
لفئة User
.- عند هذه النقطة ، يتم إنشاء مثيل للفئة.
هذا الوصف ، بالطبع ، لا يغطي جميع الفروق الدقيقة في استخدام 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()
يساعد استخدام الفئات المجردة على إصلاح واجهة الفئة الأساسية على الفور وتجنب أخطاء الميراث المستقبلية ، على سبيل المثال الأخطاء المطبعية في اسم طريقة تم تجاوزها.
نظام مساعد التسجيل التلقائي
في كثير من الأحيان ، يتم استخدام metaprogramming لتنفيذ أنماط تصميم مختلفة. تقريبا أي إطار عمل معروف يستخدم metaclasses لإنشاء كائنات التسجيل . تقوم هذه الكائنات بتخزين الارتباطات بالكائنات الأخرى وتسمح بتلقيها بسرعة في أي مكان في البرنامج. فكر في مثال بسيط للتسجيل التلقائي للمكونات الإضافية لتشغيل ملفات الوسائط بتنسيقات مختلفة.
تنفيذ Metaclass:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
وإليك الإضافات نفسها ، سنأخذ تطبيق 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')
باستخدام 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():
دعونا نرى كيف تبدو مشاركاتنا الآن:
>>> 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]
نريد حفظ العداد وزيادةه عند إنشاء صف جديد ، ونريد أيضًا إنشاء رأس الجدول الناتج مسبقًا. ميتاكلاس لانقاذ!
class MetaRow(type):
يجب توضيح شيئين هنا:
- لا تحتوي فئة الصف على سمات فئة مع
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 المتقدم .