>>> stage-01/object-model.py

الوهم الشفاف

كل شيء 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.h
typedef 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.

// الخلاصة — ماذا ربطنا؟
  • الـ PyObject struct ← يفسر 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 — فمن يصف نوع النوع؟