جدول المحتويات

  • الطراز القديم مقابل فئات الطراز الجديد
    فئات الطراز القديم
    فئات جديدة الطراز
  • النوع والفئة
  • تعريف الفصل ديناميكيًا
    مثال 1
    مثال 2
    مثال 3
    مثال 4
  • Metaclass مخصص
  • هل هذا حقا ضروري؟
  • خاتمة

يشير مصطلح metaprogramming إلى إمكانية حصول البرنامج على معرفة أو التلاعب بنفسه. تدعم Python شكلاً من أشكال البرمجة الوصفية للفئات تسمى metaclasses.

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

ومع ذلك ، عندما تظهر الحاجة ، توفر Python قدرة لا تدعمها جميع اللغات الموجهة للكائنات: يمكنك الحصول على الغطاء وتحديد الفئات الوصفية المخصصة. يعد استخدام metaclasses المخصص مثيرًا للجدل إلى حد ما ، كما هو مقترح في الاقتباس التالي من Tim Peters ، معلم Python الذي قام بتأليف Zen of Python:

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

– تيم بيترز

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

ومع ذلك ، فإن فهم Python metaclasses مفيد ، لأنه يؤدي إلى فهم أفضل للداخلية في صفوف Python بشكل عام. أنت لا تعرف أبدًا: قد تجد نفسك يومًا ما في واحدة من تلك المواقف التي تعرف فيها فقط أن metaclass المخصص هو ما تريده.

الطراز القديم مقابل فئات الطراز الجديد

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

فئات الطراز القديم

في الفصول ذات النمط القديم ، لا يكون الفصل والنوع نفس الشيء تمامًا. يتم دائمًا تنفيذ مثيل لفئة ذات نمط قديم من نوع مضمن واحد يسمى مثيل. إذا كان obj مثيلًا لفئة ذات نمط قديم ، فإن obj .__ class__ تحدد الفئة ، لكن النوع (obj) هو دائمًا مثيل. المثال التالي مأخوذ من Python 2.7:

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

فئات جديدة الطراز

توحد الفصول ذات النمط الجديد مفاهيم الفئة والنوع. إذا كان obj مثيلًا لفئة ذات نمط جديد ، فإن النوع (obj) هو نفسه obj .__ class__:

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True
>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

النوع والفئة

في Python 3 ، كل الفئات هي فئات جديدة. وبالتالي ، في Python 3 ، من المعقول الإشارة إلى نوع الكائن وفئته بالتبادل.

ملاحظة: في Python 2 ، تكون الفئات هي النمط القديم افتراضيًا. قبل Python 2.2 ، لم تكن الفئات ذات النمط الجديد مدعومة على الإطلاق. من Python 2.2 فصاعدًا ، يمكن إنشاؤها ولكن يجب الإعلان عنها صراحة على أنها نمط جديد.

تذكر أن كل شيء في بايثون هو كائن. الفئات هي كائنات أيضًا. نتيجة لذلك ، يجب أن يكون للفصل نوع. ما هو نوع الفصل؟

ضع في اعتبارك ما يلي:

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

نوع x هو فئة Foo ، كما تتوقع. لكن نوع Foo ، الفئة نفسها ، هي النوع. بشكل عام ، نوع أي فئة ذات نمط جديد هو النوع.

نوع الفصول المضمنة التي تعرفها هو أيضًا نوع:

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

لهذه المسألة ، نوع النوع هو النوع أيضًا (نعم ، حقًا):

>>> type(type)
<class 'type'>

النوع هو metaclass ، والفئات هي أمثلة. تمامًا كما أن الكائن العادي هو مثيل لفئة ، فإن أي فئة ذات نمط جديد في Python ، وبالتالي أي فئة في Python 3 ، هي مثيل من النوع metaclass.

في الحالة أعلاه:

  • x هو مثيل للفئة Foo.
  • Foo هو مثيل من النوع metaclass.
  • النوع هو أيضًا مثيل من النوع metaclass ، لذا فهو مثيل لنفسه.

تعريف الفصل ديناميكيًا

دالة type () المضمنة ، عند تمرير وسيطة واحدة ، ترجع نوع الكائن. بالنسبة لفئات النمط الجديد ، يكون هذا بشكل عام هو نفسه سمة __class__ للكائن:

>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

يمكنك أيضًا استدعاء النوع () بثلاث وسيطات – النوع (<name> ، <bases> ، <dct>):

  • <name> يحدد اسم الفئة. تصبح هذه سمة __name__ للفئة.
  • يحدد <bases> مجموعة من الفئات الأساسية التي ترث منها الفئة. تصبح هذه السمة __bases__ للفصل.
  • يحدد <dct> قاموس مساحة اسم يحتوي على تعريفات لجسم الفئة. تصبح هذه سمة __dict__ للطبقة.

يؤدي استدعاء type () بهذه الطريقة إلى إنشاء مثيل جديد من النوع metaclass. بمعنى آخر ، يتم إنشاء فئة جديدة ديناميكيًا.

في كل من الأمثلة التالية ، يُعرِّف المقتطف العلوي فئة ديناميكيًا بالنوع () ، بينما يُعرِّف المقتطف أدناه الفئة بالطريقة المعتادة ، باستخدام عبارة الفئة. في كل حالة ، المقتطفان متكافئان وظيفيًا.

مثال 1

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

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>
>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

مثال 2

هنا ، <bases> عبارة عن مجموعة تحتوي على عنصر واحد Foo ، تحدد الفئة الرئيسية التي يرث Bar منها. يتم وضع السمة ، Attr ، في البداية في قاموس مساحة الاسم:

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)
>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

مثال 3

هذه المرة ، أصبحت <bases> فارغة مرة أخرى. يتم وضع كائنين في قاموس مساحة الاسم عبر الوسيطة <dct>. الأول هو سمة تسمى attr والثاني وظيفة تسمى attr_val ، والتي تصبح طريقة للفئة المحددة:

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100
>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

مثال 4

يمكن تعريف الوظائف البسيطة جدًا فقط باستخدام lambda في Python. في المثال التالي ، يتم تعريف دالة أكثر تعقيدًا من الخارج ثم يتم تعيينها إلى attr_val في قاموس مساحة الاسم عبر الاسم f:

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100
>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

Metaclass مخصص

ضع في اعتبارك مرة أخرى هذا المثال البالي:

>>> class Foo:
...     pass
...
>>> f = Foo()

ينشئ التعبير Foo () مثيلًا جديدًا للفئة Foo. عندما يواجه المترجم Foo () ، يحدث ما يلي:

  • يتم استدعاء طريقة __call __ () لفئة Foo الرئيسية. نظرًا لأن Foo هي فئة نمطية جديدة قياسية ، فإن صنفها الرئيسي هو النوع metaclass ، لذلك يتم استدعاء طريقة type’s __call __ ().

  • تستدعي طريقة __call __ () هذه بدورها ما يلي:
    __جديد__()
    __فيه__()

إذا لم يعرّف Foo __new __ () و __init __ () ، يتم توريث الطرق الافتراضية من أصل Foo. ولكن إذا حدد Foo هذه الأساليب ، فإنها تلغي تلك الموجودة في الأصل ، مما يسمح بسلوك مخصص عند إنشاء مثيل Foo.

في ما يلي ، يتم تعريف طريقة مخصصة تسمى new () وتعيينها على أنها طريقة __new __ () لـ Foo:

>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

يعدل هذا سلوك إنشاء مثيل للفئة Foo: في كل مرة يتم إنشاء مثيل لـ Foo ، يتم تهيئته افتراضيًا بسمة تسمى attr ، والتي لها قيمة 100. (عادةً ما يظهر رمز مثل هذا في طريقة __init __ () وليس عادةً في __new __ (). تم إنشاء هذا المثال لأغراض توضيحية.)

الآن ، كما تم التأكيد عليه بالفعل ، تعتبر الفئات كائنات أيضًا. لنفترض أنك أردت تخصيص سلوك إنشاء مثيل بشكل مشابه عند إنشاء فئة مثل Foo. إذا كنت ستتبع النمط أعلاه ، فعليك مرة أخرى تحديد طريقة مخصصة وتعيينها على أنها طريقة __new __ () للفئة التي يمثل Foo مثيلًا لها. Foo هو مثيل من النوع metaclass ، لذلك تبدو الشفرة كما يلي:

# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

باستثناء ، كما ترى ، لا يمكنك إعادة تعيين طريقة __new __ () من النوع metaclass. بايثون لا تسمح بذلك.

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

أحد الحلول الممكنة هو metaclass مخصص. بشكل أساسي ، بدلاً من الالتفاف حول النوع metaclass ، يمكنك تحديد metaclass الخاص بك ، والذي يشتق من الكتابة ، ومن ثم يمكنك التخلص منه بدلاً من ذلك.

الخطوة الأولى هي تحديد metaclass المشتق من الكتابة ، على النحو التالي:

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

فئة رأس التعريف Meta (type): تحدد أن Meta مشتق من النوع. نظرًا لأن الكتابة هي metaclass ، فهذا يجعل Meta metaclass أيضًا.

لاحظ أنه تم تعريف طريقة __new __ () المخصصة للميتا. لم يكن من الممكن القيام بذلك على نوع metaclass مباشرة. تقوم طريقة __new __ () بما يلي:

  • المفوضين عبر () super إلى طريقة __new __ () الخاصة بالفئة الوصفية الأصلية (النوع) لإنشاء فصل دراسي جديد بالفعل
  • يعيّن سمة السمة المخصصة للفئة بقيمة 100
  • إرجاع الفصل الذي تم إنشاؤه حديثًا

الآن النصف الآخر من الشعوذة: حدد فئة جديدة Foo وحدد أن metaclass الخاص بها هو Metaclass Meta المخصص ، بدلاً من نوع metaclass القياسي. يتم ذلك باستخدام الكلمة الأساسية metaclass في تعريف الفئة على النحو التالي:

>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

هاهو! اختار Foo سمة Attr تلقائيًا من Meta metaclass. بالطبع ، أي فئات أخرى تحددها بالمثل ستفعل الشيء نفسه:

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

بالطريقة نفسها التي يعمل بها الفصل كقالب لإنشاء الكائنات ، يعمل metaclass كقالب لإنشاء الفئات. يشار إلى Metaclasses أحيانًا بمصانع الطبقة.

قارن بين المثالين التاليين:

مصنع الكائن:

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100

مصنع كلاس:

>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

هل هذا حقا ضروري؟

بسيط مثل مثال مصنع الفئة أعلاه ، فهو جوهر كيفية عمل metaclasses. أنها تسمح بتخصيص إنشاء مثيل للفئة.

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

في بايثون ، هناك طريقتان أخريان على الأقل يمكن من خلالها تحقيق الشيء نفسه بشكل فعال:

الميراث البسيط:

>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

مصمم فئة:

>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

خاتمة

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