>>> stage-06/protocols.py

لعبة البروتوكولات

C slots, descriptors, ABCs, Protocols — كيف تتفاهم كائنات بايثون دون عقود مكتوبة؟

أي من هذه الكائنات تعمل مع for x in obj؟ توقّع قبل أن تجرّب:

class A:
    def __getitem__(self, idx):
        return idx * 2

class B:
    def __iter__(self):
        return iter([1, 2, 3])

class C:
    pass

class D(int):
    def __iter__(self):
        return iter([1, 2, 3])

# أي منها يعمل مع for؟ وأي منها مع iter()؟
# أي منها مع list()؟ وأي منها مع reversed()؟

ثم: ما الذي يجعل @dataclass تنشئ __init__ تلقائياً؟

// "ليش" — الدافع التصميمي

بايثون لغة "duck typing" — "إذا مشى كالبطة وصوّت كالبطة فهي بطة". هذا يعني أن interfaces formal غير موجودة (كل شيء optional). لكن الـ interpreter يحتاج way يعرف بها إذا كان object "قابل للتكرار" أو "قابل للاستدعاء" إلخ. الحل: slots في الـ type object.

Slots — جسر C إلى بايثون

كل PyTypeObject عنده مصفوفة من الـ slots. عندما تكتب for x in obj:

  1. ابحث عن tp_iter (__iter__). إن وُجد، استدعِه → iterator.
  2. إن لم يُوجد، ابحث عن tp_as_sequence.sq_item (__getitem__). إن وُجد، استعمل iteration protocol بالـ index من 0.
  3. إن لم يُوجد → TypeError.

هذا التسلسل موجود في cpython/Objects/abstract.cPyObject_GetIter.

Protocols الرئيسية

Protocol C Slot Python Method
Iteration tp_iter / tp_iternext __iter__ / __next__
Sequence sq_item / sq_length __getitem__ / __len__
Number nb_add / nb_sub __add__ / __sub__
Callable tp_call __call__
Descriptor tp_descr_get / tp_descr_set __get__ / __set__
Context mgr am_enter / am_exit __enter__ / __exit__

المراسلات في cpython/Objects/typeobject.c: دالة slot_tp_init تتعقب __init__ method وتحوّلها إلى tp_init slot.

ABCs vs Protocols

ABCs: ABCMeta يضيف __instancecheck__ و __subclasscheck__. يمكن تسجيل virtual subclass.

Protocols (PEP 544): Structural subtyping — إذا كان للكلاس نفس methods، هو subclass بغض النظر عن الوراثة. @runtime_checkable يعمل عبر __instancecheck__ الذي يتحقق من وجود الـ methods المطلوبة.

Descriptor Protocol — تحت الـ property

class Property:
    def __init__(self, fget): self.fget = fget
    def __get__(self, obj, objtype=None):
        if obj is None: return self
        return self.fget(obj)

عندما تنفّذ obj.x، بايثون تبحث في type(obj).__mro__ عن x، تجده descriptor، تستدعي __get__(property_obj, obj, type(obj)) الذي يستدعي self.fget(obj).

NotImplemented و binary protocols

عندما بايثون تنفّذ a + b:

  1. استدعِ a.__add__(b). لو رجع NotImplemented:
  2. استدعِ b.__radd__(a). لو رجع NotImplemented:
  3. ارفع TypeError.

هذا يسمح للـ type الأيمن بأن "يخطف" العملية.

الممنوع: استخدام @property أو @classmethod أو @staticmethod أو __getattr__.

المطلوب: اكتب ثلاثة كلاسات تحاكي behaviour الـ @property و @classmethod و @staticmethod — لكن بدون استخدامهم. استخدم فقط الـ descriptor protocol (__get__ و __set__ و __delete__):

class my_property:
    # obj.x → 42, obj.x = 5 → AttributeError
    pass

class my_classmethod:
    # يصنع method مرتبط بالكلاس
    pass

class my_staticmethod:
    # يصنع method لا يأخذ self ولا cls
    pass

ثم اختبر. كل class من الثلاثة يجب أن يكون "descriptor" حقيقي. my_property يجب أن يدعم setter و deleter مثل property الحقيقية.

مكافأة: اشرح لماذا t.x = 5 مع my_property دون setter يرفع AttributeError وليس TypeError.

// الخلاصة — ماذا ربطنا؟
  • كل __method__ هو واجهة لـ C slot في PyTypeObject
  • الـ protocols: iteration → tp_iter/tp_iternext؛ number → nb_add/nb_sub...
  • ABCs تضيف __instancecheck__ لتعطي virtual inheritance
  • Protocols (PEP 544) structural subtyping: شكل الكلاس لا نسبه
  • Descriptors: أساس properties, classmethods, staticmethods
  • NotImplemented: بروتوكول ثنائي للـ binary operators

النهاية — لا بذرة غموض. الخريطة الآن مكتملة. الرحلة انتهت. لكن هناك دائماً ما هو أعمق: اقرأ PEP 703 (nogil)، اقرأ الـ JIT compilers (PyPy, Numba)، اقرأ cpython/ كاملاً. أنت الآن تعرف أين تبحث.