الوهم الشفاف
كل شيء object، لكن ماذا يعني "كائن" في لغة كُتبت بلغة C تحتاج لتعقب كل كيلوبايت؟
شغّل هذا الكود. لكن قبل أن تشغّله، اشرح كل سطر — بالضبط ماذا يحدث في الذاكرة:
>>> import sys
>>> x = 256
>>> y = 256
>>> print(x is y) # ؟
>>> a = 10**6
>>> b = 10**6
>>> print(a is b) # ؟
>>> s1 = "hello"
>>> s2 = "hello"
>>> print(s1 is s2) # ؟
>>> s3 = "hello world!"
>>> s4 = "hello world!"
>>> print(s3 is s4) # ؟
لماذا بعض القيم تشترك في id وبعضها لا؟ اشتقّ الشرط من مبدأ واحد: كيف تمثّل C عدداً صحيحاً في struct؟
بايثون كُتبت بلغة C. C لا تعرف objects. تعرف structs. إذاً "كل شيء object" تعني: كل قيمة بايثون تُخزَّن في struct C. وهذا الـ struct له overhead ثابت. فهم هذا الـ struct هو المفتاح لكل شيء بعد الآن.
PyObject — البنية الأساسية
افتح cpython/Include/object.h. ابحث عن PyObject:
object.htypedef struct _object { Py_ssize_t ob_refcnt; // reference count PyTypeObject *ob_type; // pointer to the type } PyObject;
هذا هو كل object بايثون بالضبط: 16 بايتاً (على نظام 64-bit) — عداد مرجعي ومؤشر للنوع. والـ ob_size للمتغيّرات الحجم:
typedef struct {
PyObject ob_base; // PyObject first
Py_ssize_t ob_size; // number of items
} PyVarObject;
هذا يعني: list و str و bytes و tuple — كلها تبدأ بـ ob_refcnt + ob_type + ob_size.
id(x) ترجع عنوان ob_refcnt في الذاكرة — أي (void*)obj. x is y تقارن عنوان الـ struct — (void*)x == (void*)y. type(x) تقرأ ob_type من الـ struct.
لماذا الأعداد الصغيرة "مشتركة"؟
افتح cpython/Objects/longobject.c وابحث عن small_ints. ستجد مصفوفة من PyLongObject مخزّنة مسبقاً للأعداد من -5 إلى 256. هذه الأعداد لا تُحرّر أبداً. 256 اختير لأنها 2^8 — قيمة صغيرة مضمونة لظهورها بكثرة.
Interning للنصوص
النصوص القصيرة التي تشبه identifiers تُـ intern تلقائياً في cpython/Objects/unicodeobject.c. "hello" يظهر كـ identifier في الكود، فبايثون تخزّنه في dict عالمي. "hello world!" فيه مسافة — ليس صالحاً كـ identifier — فلا يُـ intern.
الـ __slots__ والـ struct layout
// Without __slots__:
typedef struct {
PyObject_HEAD
PyObject *dict; // + 8 bytes overhead
} MyObject;
// With __slots__ = ('x',):
typedef struct {
PyObject_HEAD
PyObject *x; // stored directly in struct
} MyObject;
__slots__ يزيل __dict__ pointer من struct — لا يمنع إضافة attributes "سحرياً"، بل يزيل الحاوية التي تسمح بذلك.
Descriptors — بروتوكول الـ __get__
كيف تعمل @property و @classmethod و staticmethod؟ هي كائنات implement الـ descriptor protocol: __get__(self, obj, objtype). اقرأ cpython/Objects/descrobject.c.
الممنوع: استخدام type() أو isinstance() أو أي دالة مدمجة للـ type checking.
المطلوب: اكتب دالة my_type_of(obj) ترجع type أي object بقراءة الذاكرة مباشرة باستخدام ctypes:
import ctypes
def my_type_of(obj):
# اقرأ ob_type pointer من offset 8 (بعد ob_refcnt)
# العنوان = id(obj) + size_of_Py_ssize_t
pass
قيود إضافية: لا تستخدم type() أبداً (حتى للتحقق). ركّب الدالة بحيث تعمل لأي object (int, str, list, class, function). اشرح لماذا تحتاج أن تحسب offset لكل حقل.
مكافأة: اكتب my_isinstance(obj, typ) تتحقق من MRO يدوياً بقراءة tp_base و tp_bases من الـ PyTypeObject struct.
- الـ
PyObjectstruct ← يفسرidوisوtype - الـ
ob_refcnt← يفسر الـ reference counting (سنفصّله في Stage 04) - الـ
ob_type← يفسر MRO و attribute lookup و type system - الـ
__slots__← يفسر struct layout والـ memory overhead - Descriptors ← يفسر properties و methods و classmethods
إذا كان كل object مجرد struct، فكيف تعرف بايثون كم عدد البايتات التي يجب قراءتها من الذاكرة لكل object؟ أين تخزّن هذه المعلومة؟ الجواب يأخذك إلى ob_type نفسه — لأن type object عنده tp_basicsize و tp_itemsize. لكن هذا يعني: النوع نفسه object — فمن يصف نوع النوع؟