هر نام کجا زندگی میکند؟
وقتی در کد نامی مثل rate را میخوانید، پایتون باید بفهمد این نام به کدام شیء اشاره دارد. پاسخ از قاعدهی LEGB میآید؛ پایتون به این ترتیب جستوجو میکند و اولین مورد را برمیدارد:
| حرف | دامنه | مثال |
|---|---|---|
| L — Local | داخل همین تابع | پارامترها و متغیرهای تعریفشده در تابع |
| E — Enclosing | تابع بیرونی (در توابع تودرتو) | متغیرهای تابعی که این تابع را در خود دارد |
| G — Global | سطح ماژول (فایل) | ثابتها و توابع تعریفشده در بالای فایل |
| B — Built-in | نامهای داخلی پایتون | len، print، sum |
TAX_RATE = 0.10 # Global
def invoice(amount):
discount = 0.05 # Local
def apply():
return amount * (1 - discount) * (1 + TAX_RATE) # E و G
return round(apply())
print(invoice(10_000_000)) # 10450000
print(discount) # NameError — discount بیرون تابع وجود ندارد
متغیرهای محلی با پایان تابع از بین میروند و هر فراخوانی، مجموعهی محلی تازهی خودش را دارد. این جداسازی چیز خوبی است: تابع با دنیای بیرون فقط از راه پارامتر و return حرف میزند.
دام UnboundLocalError
count = 0
def register():
count += 1 # UnboundLocalError!
return count
چرا خواندن متغیر global مجاز است ولی این کد خطا میدهد؟ چون پایتون در زمان کامپایل تصمیم میگیرد: هر نامی که داخل تابع به آن انتساب شود (از جمله با +=)، در کل آن تابع محلی است. پس count محلی حساب میشود و هنوز مقدار نگرفته است.
global و nonlocal
count = 0
def register():
global count # «منظورم همان count سطح ماژول است»
count += 1
def make_counter():
n = 0
def step():
nonlocal n # «منظورم n تابع بیرونی است»
n += 1
return n
return step
next_id = make_counter()
next_id(); next_id()
print(next_id()) # 3
مثال دوم یک closure است: تابع داخلی متغیر تابع بیرونی را حتی پس از پایان آن «به خاطر میسپارد». این پایهی decorator هاست که در پایان دوره به آن اشاره میکنیم.
چرا global بد نام است
متغیر global را هر تابعی میتواند عوض کند؛ پس وقتی مقدارش غلط است، نمیدانید مقصر کیست. تست کردن توابعی که به global وابستهاند هم سخت است. قاعده: ثابتهای global (با حروف بزرگ) خوباند؛ حالت (state) global بد است. حالت را از راه پارامتر بدهید و از راه return بگیرید، یا در یک کلاس (فصل ۷) بستهبندی کنید.
نکتههایی که کمتر کسی میداند
- برای تغییر محتوای یک لیست یا دیکشنری global به global نیاز ندارید (
orders.append(x)کار میکند)؛ global فقط برای انتساب دوبارهی نام لازم است. - حلقهی for و بلوک if در پایتون scope جدید نمیسازند؛ متغیر تعریفشده داخل if بعد از آن هم در دسترس است.
globals()وlocals()دیکشنری نامها را برمیگردانند؛ برای دیباگ مفید، برای کد اصلی ممنوع.- closure ها متغیر را «زنده» نگه میدارند، نه مقدارش را؛ به همین دلیل lambda های ساختهشده در حلقه همه آخرین مقدار را میبینند (درس آخر همین فصل).
- اگر نام یک built-in را در سطح ماژول بازتعریف کنید (مثلاً
input = ...)، طبق LEGB همهی توابع آن فایل نسخهی شما را میبینند.