لعبة البروتوكولات
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:
- ابحث عن
tp_iter(__iter__). إن وُجد، استدعِه → iterator. - إن لم يُوجد، ابحث عن
tp_as_sequence.sq_item(__getitem__). إن وُجد، استعمل iteration protocol بالـ index من 0. - إن لم يُوجد →
TypeError.
هذا التسلسل موجود في cpython/Objects/abstract.c — PyObject_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:
- استدعِ
a.__add__(b). لو رجعNotImplemented: - استدعِ
b.__radd__(a). لو رجعNotImplemented: - ارفع
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/ كاملاً. أنت الآن تعرف أين تبحث.