>>> stage-02/names.py

الأسماء ليست صناديق

Names vs boxes, frames, closures, LEGB — نموذج ذهني بايثون الحقيقي

اشرح سلوك هذا الكود دون تشغيله:

def append_to(element, target=[]):
    target.append(element)
    return target

print(append_to(1))      # ?
print(append_to(2))      # ?
print(append_to(3, []))  # ?
print(append_to(4))      # ?

ثم:

funcs = []
for i in range(5):
    funcs.append(lambda: i)

print([f() for f in funcs])  # ?

و:

x = [1, 2, 3]
y = x
x = x + [4]
print(x, y)  # ?

x = [1, 2, 3]
y = x
x += [4]
print(x, y)  # ?

لماذا x + [4] و x += [4] نتائجهما مختلفة على y؟

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

بايثون ليس لغة "variable" — هي لغة "names bound to objects". الفرق ليس أكاديمياً. الفرق يحدد: كيف تفهم aliasing (y = x لا تنسخ)، كيف تفهم default arguments (تُقيَّم مرة واحدة عند تعريف الدالة)، كيف تفهم closures (i مرتبط بالاسم، لا بالقيمة وقت الإنشاء)، كيف تفهم augmented assignment (x += 1 يستدعي __iadd__ إن وُجد).

Names و Frames

كل اسم يعيش في namespacedict من names → objects. الـ namespaces ثلاثة: locals, globals, builtins. LEGB = Lookup chain: 1. Local (function frame → f_locals2. Enclosing (outer functions)، 3. Global (module namespace)، 4. Builtins (builtins module).

هذا ليس مجرد قاعدة — هو كود حقيقي. اقرأ cpython/Python/ceval.c: ابحث عن LOAD_NAME و LOAD_GLOBAL و LOAD_FAST و LOAD_DEREF. كل واحدة تبحث في namespace مختلف. الـ LOAD_FAST هو الأسرع لأنه يقرأ من مصفوفة (array) محلية في الـ frame — لا lookup في dict.

Default Arguments — الـ "bug" الأكثر شهرة

def f(x=[]): هذا السطر يُنفَّذ مرة واحدة: عند تعريف الدالة. الـ [] يُخلق object list واحد ويُربط بالـ __defaults__ tuple في كائن الدالة. كل استدعاء لـ f يستخدم نفس الـ list object. الدليل: print(f.__defaults__) يظهر لك الـ list نفسه.

Closures — Late Binding

funcs = [lambda: i for i in range(5)] يخلق 5 closures. كل واحد يقرأ i من الـ enclosing scope. لكن i هو اسم في scope الحلقة — كل closures يشاركون نفس الاسم i. وقت تنفيذ الـ lambda، قيمة i هي آخر قيمة (4). الحل المعروف: lambda i=i: i — يربط القيمة الحالية كـ default parameter (يُقيَّم فوراً).

Augmented Assignment — x += 1 لا يساوي x = x + 1

لقيم mutable: x += 3 calls x.__iadd__([3]) — modifies in-place. x = x + [4] calls x.__add__([4]) — creates new list. الفرق بين __iadd__ (in-place) و __add__ (new object).

الممنوع: استخدام nonlocal أو global keywords.

المطلوب: اكتب دالة make_counter() ترجع دالتين: الأولى get() ترجع العداد الحالي، الثانية inc() تزيد العداد بواحد. باستخدام closures فقط — لا class ولا dict ولا list ولا object. ثم اشرح لماذا nonlocal ممنوع — هذا سيجبرك على فهم كيف تخزّن بايثون mutable state في closure بدون nonlocal. ثم: استخدم dis.dis(make_counter) لتتبّع LOAD_CLOSURE و LOAD_DEREF.

def make_counter():
    # ؟
    pass
// الخلاصة — ماذا ربطنا؟
  • Names != boxes: هذا النموذج يفسر default args, closures, aliasing
  • LEGB ليس قاعدة نظرية — هو chain من dicts و arrays في frame
  • LOAD_FAST vs LOAD_GLOBAL: فرق performance له جذر في structure البيانات
  • Augmented assignment: __iadd__ vs __add__ — قرار design يمنح performance للمتغيّرات لكنه يخلط المبتدئين

أنت الآن تعرف أن الـ frame objects تخزّن الـ locals في dict أو array. لكن ما الذي يحدد أيهما يُستخدم؟ الجواب يأتي من المترجم — الـ compiler يقرر في compile time ما إذا كانت دالة تستخدم exec() أو eval() فيقرر نوع storage. هذا يقودك للإقليم الثالث.