توابع

حوزهٔ متغیرها (Scope) در پایتون: محلی، سراسری و UnboundLocalError

۴ دقیقه مطالعه · به‌روزرسانی ۲۲ مهر ۱۴۰۵

وقتی داخل یک تابع متغیری به اسم x می‌سازید و بیرون تابع هم یک x دارید، آیا این دو یکی‌اند؟ جواب این سؤال، مفهوم حوزه (scope) است و فهمیدنش جلوی خیلی از باگ‌های گیج‌کننده را می‌گیرد.

قانون اصلی

هر بار که تابعی صدا زده می‌شود، پایتون برایش یک «فضای نام» (namespace) جدید و موقت می‌سازد. پارامترها و هر متغیری که داخل تابع ساخته شود، در همین فضا زندگی می‌کنند و وقتی تابع تمام شد، از بین می‌روند.

دو قاعده:

  1. داخل یک تابع می‌شود به متغیری که بیرون تعریف شده دسترسی داشت (خواندنش).
  2. داخل یک تابع نمی‌شود متغیری را که بیرون تعریف شده تغییر داد (دوباره به آن مقدار داد). اگر سعی کنید، در واقع یک متغیر محلی جدید با همان اسم ساخته‌اید.

سه مثال

قبل از اجرای هر کدام، خروجی را حدس بزنید.

مثال ۱: f، متغیر محلی هم‌نام

def f(y):
    x = 1
    x += 1
    print(x)

x = 5
f(x)
print(x)

خروجی: 2 و بعد 5. داخل f، خط x = 1 یک x جدید و محلی می‌سازد که هیچ ربطی به x بیرونی ندارد. x بیرونی دست‌نخورده 5 می‌ماند.

مثال ۲: g، خواندن متغیر بیرونی

def g(y):
    print(x)
    print(x + 1)

x = 5
g(x)
print(x)

خروجی: 5، 6، 5. داخل g هیچ‌جا به x مقدار داده نشده، پس پایتون آن را در فضای بیرونی پیدا می‌کند و می‌خواندش. کاملاً مجاز است.

مثال ۳: h، تلاش برای تغییر

def h(y):
    x = x + 1

x = 5
h(x)
print(x)

اجرا کنید:

UnboundLocalError: cannot access local variable 'x' where it is not associated with a value

چرا؟ چون داخل h یک انتساب به x هست (x = …)، پایتون موقع تعریف تابع تصمیم می‌گیرد که x در کل این تابع محلی است. وقتی به x + 1 می‌رسد، می‌خواهد x محلی را بخواند که هنوز مقدار نگرفته است.

💡 این تصمیم برای کل تابع گرفته می‌شود، نه فقط از آن خط به بعد. چالش ۲ دقیقاً همین تله را دارد.

ردگیری فضاهای نام

مثال پیچیده‌تر با یک تابع داخل تابع دیگر:

def g(x):
    def h():
        x = 'abc'
    x = x + 1
    print('in g(x): x =', x)
    h()
    return x

x = 3
z = g(x)
print('z =', z, '| global x =', x)

فضاهای نام قدم به قدم:

لحظه فضای سراسری فضای g فضای h
بعد از x = 3 g: (کد تابع)، x = 3
ورود به g(x) همان x = 3
بعد از def h همان x = 3، h: (کد تابع)
بعد از x = x + 1 همان x = 4، h
داخل h() همان x = 4، h x = 'abc' (محلیِ h)
بعد از برگشت از h همان x = 4، h (از بین رفت)
بعد از return x g، x = 3، z = 4 (از بین رفت)

خروجی: in g(x): x = 4 و بعد z = 4 | global x = 3.

نکته‌ها:

  • x سراسری هیچ‌وقت عوض نشد (همیشه ۳).
  • پارامتر x در g یک کپی از نام است که به همان مقدار ۳ اشاره می‌کند؛ x = x + 1 فقط x محلیِ g را عوض کرد.
  • x = 'abc' داخل h هم یک متغیر محلیِ سوم ساخت که روی x تابع g اثری نداشت.
  • تنها راه درست برای بیرون فرستادن نتیجه از تابع، return است.

global: اگر واقعاً لازم بود

با کلمهٔ کلیدی global می‌شود به پایتون گفت «این x همان x سراسری است»:

counter = 0

def increment():
    global counter
    counter += 1

increment()
increment()
print(counter)

⚠️ ولی تا جای ممکن از global استفاده نکنید. وقتی هر تابعی بتواند متغیرهای سراسری را عوض کند، فهمیدن اینکه یک مقدار کِی و کجا تغییر کرده کابوس می‌شود. راه بهتر: ورودی را پارامتر بگیرید و نتیجه را return کنید:

def increment(counter):
    return counter + 1

counter = increment(counter)

تمرین

خروجی این کد چیست؟

a = 10

def first():
    a = 20
    second()
    print("first:", a)

def second():
    print("second:", a)

first()
print("global:", a)
جواب
second: 10
first: 20
global: 10

نکتهٔ گول‌زننده خط اول است. ممکن است فکر کنید second چون از داخل first صدا زده شده، a = 20 را می‌بیند. ولی پایتون متغیرها را بر اساس جایی که تابع تعریف شده پیدا می‌کند، نه جایی که صدا زده شده. second در سطح سراسری تعریف شده، پس a سراسری (۱۰) را می‌بیند.