>>> stage-03/bytecode.py

بين النص والآلة

Source → bytecode → VM stack-based — الـ dis بوصلتك

اكتب دالة بايثون بسيطة:

def mystery(x, y):
    result = []
    for i in range(x):
        if i % 2:
            result.append(i * y)
    return result

بدون تشغيل الدالة، استخدم dis.dis(mystery) فقط لتتنبأ بعدد مرات تنفيذ كل bytecode instruction إذا استدعينا mystery(100, 3). ثم اشرح لماذا الـ dis يظهر POP_JUMP_IF_FALSE قبل FOR_ITER رغم أن الـ if بعد الـ for في الكود المصدري.

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

Source code هو نص للإنسان. الآلة تحتاج تعليمات. التعليمات الوسيطة (bytecode) هي حل وسط بين readability والسرعة.

مراحل التحويل

Source → Tokenizer → Parser → AST → Symbol Table → Compiler → Bytecode. كل مرحلة موجودة كملف في cpython/Python/: tokenize.c، ast.c، symtable.c، compile.c، ceval.c.

الـ .pyc ماذا يحتوي؟

افتح أي .pyc بهيكس dump: Magic Number (2 bytes) | Flags (2 bytes) | Timestamp (4/8 bytes) | → Marshalled Code Object. الـ magic number يتغير مع كل إصدار.

Code object يحتوي:

co_argcount, co_nlocals, co_code (bytes of bytecode),
co_consts (tuple of constants), co_names (tuple of names),
co_varnames, co_freevars, co_cellvars.

الـ VM — Stack-Based

لماذا stack؟ التصميم أبسط من register machine:

LOAD_FAST    0    # push local[0] (x) to stack
LOAD_CONST   1    # push 1
BINARY_ADD         # pop 2, add, push result
RETURN_VALUE       # pop, return

اقرأ cpython/Python/ceval.c — الـ _PyEval_EvalFrameDefault هي الـ main loop: infinite loop يقرأ instruction، ينفذها، يقفز.

Closures بالـ bytecode

def outer(x):
    def inner():
        return x
    return inner

dis سترى MAKE_FUNCTION و LOAD_CLOSURE و LOAD_DEREF. الـ compiler يرى في symbol table أن x مستخدم في inner لكنه معرّف في outer، فيقرر تخزينه في "cell". الـ cell هو box قابل للتعديل يخزّن قيمة يمكن مشاركتها بين frames.

Generators

أي دالة فيها yield تصبح generator function. الـ compiler يضع CO_GENERATOR flag في code object. ceval.c يعامل هذه الدوال بطريقة خاصة: لا تنفّذ كل شيء دفعة واحدة، بل ترجع PyGenObject يحوي gi_frame (frame معلّق). في كل next(gen) أو gen.send()، تستأنف الـ frame من آخر yield. اقرأ cpython/Objects/genobject.c.

async/await — هي generators تحت الغطاء

قبل Python 3.5، yield from كان يستخدم لبناء coroutines. الـ await في الجوهر هو yield from مع بعض الفروق. async def يضع CO_COROUTINE flag.

الممنوع: استخدام yield أو async def أو أي مكتبة.

المطلوب: اكتب دالة my_range(n) تحاكي range — لكنها ترجع كائناً قابلاً للتكرار (iterable). هذا الكائن لا يحوي القيم كلها في الذاكرة. يجب أن ينتج القيم واحدة تلو الأخرى باستخدام state machine يدوي وبروتوكول الـ __iter__/__next__.

القيود: لا تستخدم yield (المطلوب أن تبني الـ state machine يدوياً، مثلما يفعل المترجم بالضبط مع generators). لا تخزّن القيم كلها — خزّن فقط state (العداد الحالي). ركّب الـ dis output لدالتك واشرح كل instruction. ثم اقترح كيف يمكن لـ CPython تحسين دالتك لتكون أسرع.

// الخلاصة — ماذا ربطنا؟
  • Source → bytecode → execution: ثلاث طبقات، كل طبقة في ملف منفصل
  • الـ .pyc هو serialized code object مع magic number
  • الـ VM stack-based: كل instruction يعدّل stack
  • Closures: cells في symbol table تتحول إلى LOAD_CLOSURE/LOAD_DEREF
  • Generators: frames معلّقة مع state machine
  • async: syntactic sugar فوق yield from مع flag مختلف

إذا كان الـ bytecode هو تعليمات لـ VM، فمن الذي ينفّذ الـ VM نفسه؟ الحلقة اللانهائية في ceval.c. لكن هذه الحلقة تستخدم switch-case ضخماً — وكل case له تكلفة. كيف تخفف C بايثون هذه التكلفة؟ الجواب: computed goto (GCC extension) بدل switch. لكن ماذا عن الأنظمة التي لا تدعمه؟ هذا يقودنا للـ GIL — لأن السرعة والـ GIL متصلان.