2.16. النطاق

عندما يبحث Python عن اسم داخل دالة، فإنه يبحث في تسلسل محدد من النطاقات. فهم هذا التسلسل يفسر لماذا تحجب بعض الإسنادات الأسماء الخارجية، ولماذا تعدّلها أخرى، ولماذا تستطيع الدوال المتداخلة تذكّر القيم من حيث عُرّفت.

Nested boxes showing a local scope inside a module scope inside the built-in scope, with an arrow indicating that name lookup walks outward from the innermost frame.

يبدأ البحث عن الأسماء في النطاق المحلي للدالة ويسير نحو الخارج إلى نطاقي الوحدة والمدمج حتى يُعثر على تطابق.

2.16.1. النطاق المحلي ونطاق الوحدة

الأسماء المعرّفة داخل دالة هي محلية لتلك الدالة وتختفي عند انتهاء الاستدعاء:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

الأسماء المعرّفة على المستوى الأعلى لملف .py هي على مستوى الوحدة (وتسمى أحيانًا عمومية) وتكون مرئية في كل مكان في ذلك الملف، بما في ذلك داخل الدوال:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

قراءة اسم على مستوى الوحدة من داخل دالة أمر تلقائي. أما الإسناد إلى ذلك الاسم من داخل دالة فليس كذلك -- إذ ينشئ الإسناد متغيرًا محليًا جديدًا يحجب المتغير على مستوى الوحدة لبقية الاستدعاء:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

اسم counter على الجهة اليسرى يجعل counter اسمًا محليًا في bump، فلا تجد القراءة على اليمين قيمة لإيجادها.

2.16.1.1. الكلمة المفتاحية global

لإعادة إسناد اسم على مستوى الوحدة فعليًا من داخل دالة، صرّح به أولًا كـ global:

counter = 0

def bump():
    global counter
    counter += 1

استخدم global باعتدال. الدوال التي تغيّر حالة مخفية يصعب التفكير فيها أكثر من الدوال التي تأخذ القيم كوسائط وتعيد قيمًا جديدة. الحل المعتاد لمسألة "أحتاج إلى مشاركة الحالة" هو تمرير كائن (قائمة، أو قاموس، أو نسخة من صنف) كوسيط وتغيير ذلك الكائن بدلًا من ذلك.

2.16.2. الدوال اللامحدودة (Lambdas)

تبني عبارة lambda دالة صغيرة مجهولة الاسم في تعبير واحد:

square = lambda x: x * x
square(7)             # 49

وهي مكافئة تمامًا لـ

def square(x):
    return x * x

يجب أن يكون جسم lambda تعبيرًا واحدًا -- لا عبارات، ولا أسطر متعددة. الاستخدام الرئيسي هو تمرير دالة صغيرة كوسيط إلى شيء يأخذ دالة:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

عندما يتجاوز الجسم تعبيرًا واحدًا، انتقل إلى def حقيقية. تسمية دالة بـ def يمنحها أيضًا اسمًا في آثار التتبع (tracebacks)، وهو ما لا تملكه lambda.

2.16.3. الإغلاقات (Closures)

يمكن لدالة معرّفة داخل دالة أخرى أن تقرأ الأسماء من نطاق الدالة المحيطة. تلتقط الدالة الداخلية تلك الأسماء وتظل تعمل حتى بعد أن يعود الاستدعاء الخارجي:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

الناتج:

105 110

add5 و add10 هما دالتان منفصلتان، كل منهما تتذكر قيمة n الخاصة بها. الدالة التي تبني وتعيد دالة داخلية مخصصة بهذه الطريقة تسمى إغلاقًا (closure). وهذا هو السبب الرئيسي الذي يجعل اللغة بحاجة إلى دوال متداخلة في المقام الأول -- طريقة لتضمين بعض الحالة داخل قيمة دالة ثم تسليم تلك القيمة كقابل للاستدعاء واحد.

قراءة الأسماء الملتقطة تحدث تلقائيًا. أما إعادة الربط لأحدها فتحتاج كلمة مفتاحية إضافية. المثال أدناه يفعل ما يبدو صحيحًا ويفشل:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

الإسناد إلى count داخل tick يجعل count محليًا لـ tick، بالطريقة نفسها التي كان سيجعله بها محليًا في دالة على المستوى الأعلى. الكلمة المفتاحية nonlocal تخبر Python بأن "هذا الاسم يقيم في الدالة المحيطة، أعد ربطه هناك":

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

الناتج:

1 2 3

nonlocal بالنسبة لنطاق الدالة المحيطة هي ما تمثله global بالنسبة لنطاق الوحدة. لاحظ أن تغيير كائن ملتقط (استدعاء some_list.append(...)، some_dict[k] = v) لا يحتاج إلى nonlocal -- فالاسم لا يُعاد ربطه، وإنما يتغير الكائن الذي يشير إليه فقط.