آموزش پایتون, پایتون سطح متوسط

محدوده متغیرها در پایتون؛ تفاوت Local و Global

محدوده متغیرها در پایتون؛ تفاوت Local و Global

🗓 آخرین به‌روزرسانی:

محدوده متغیر یا Scope مشخص می‌کند هر نام در کدام بخش برنامه قابل دسترسی است و پایتون هنگام استفاده از یک متغیر، مقدار آن را از کجا پیدا می‌کند. درک تفاوت متغیرهای Local و Global برای نوشتن توابع قابل پیش‌بینی، جلوگیری از تداخل نام‌ها و رفع خطاهایی مانند NameError و UnboundLocalError ضروری است.

این مطلب، درس ششم از دوره رایگان پایتون سطح متوسط است. در درس قبلی با پارامترها و آرگومان‌های توابع آشنا شدیم. اکنون بررسی می‌کنیم متغیرهای داخل و بیرون تابع چه محدوده‌ای دارند و پایتون چگونه نام‌ها را جست‌وجو می‌کند.

Scope در پایتون چیست؟

Scope یا محدوده دسترسی، بخشی از برنامه است که در آن می‌توان از یک نام استفاده کرد. هر متغیر در یک محدوده ساخته می‌شود و لزوماً از همه بخش‌های برنامه قابل دسترسی نیست.

site_name = "Dev Source"

def show_message():
    user_name = "مهران"
    print(user_name)
    print(site_name)

show_message()

در این مثال:

  • site_name خارج از تابع ساخته شده و یک متغیر سراسری است.
  • user_name داخل تابع ساخته شده و یک متغیر محلی است.
  • تابع می‌تواند متغیر سراسری را بخواند.
  • متغیر محلی فقط در محدوده همان تابع قابل استفاده است.

متغیر محلی یا Local Variable

متغیری که داخل یک تابع تعریف می‌شود، معمولاً محلی است. این متغیر هنگام اجرای تابع ساخته می‌شود و خارج از تابع در دسترس نیست.

def calculate_total():
    price = 120000
    quantity = 3
    total = price * quantity
    print(total)

calculate_total()

متغیرهای price، quantity و total محلی هستند.

دسترسی به متغیر محلی خارج از تابع

def create_message():
    message = "سلام"

create_message()
print(message)

این کد خطای زیر را ایجاد می‌کند:

NameError: name 'message' is not defined

متغیر message فقط در محدوده تابع create_message وجود دارد.

پارامترهای تابع نیز محلی هستند

پارامترهای تابع مانند متغیرهای محلی رفتار می‌کنند:

def greet(name):
    message = f"سلام {name}"
    return message

print(greet("مهران"))

در این تابع، هم name و هم message فقط داخل تابع قابل استفاده‌اند.

هر بار فراخوانی تابع، محدوده محلی مستقل می‌سازد

def double(number):
    result = number * 2
    return result

first = double(5)
second = double(20)

print(first)
print(second)

هر فراخوانی مقدارهای محلی مستقل خود را دارد. مقدار number در فراخوانی نخست با فراخوانی دوم تداخل ندارد.

متغیر سراسری یا Global Variable

متغیری که در سطح اصلی فایل و خارج از توابع ساخته می‌شود، متغیر سراسری نام دارد:

tax_rate = 9

def show_tax_rate():
    print("نرخ مالیات:", tax_rate)

show_tax_rate()
print(tax_rate)

متغیر tax_rate هم در سطح اصلی برنامه و هم برای خواندن در داخل تابع قابل دسترسی است.

خواندن متغیر Global داخل تابع

برای خواندن یک متغیر سراسری نیازی به کلمه کلیدی خاصی نداریم:

currency = "تومان"

def format_price(price):
    return f"{price:,} {currency}"

print(format_price(250000))

تابع مقدار currency را می‌خواند، اما آن را تغییر نمی‌دهد.

ساخت متغیر هم‌نام داخل تابع

اگر داخل تابع به نامی مقدار بدهیم، پایتون آن نام را محلی در نظر می‌گیرد:

language = "Python"

def change_language():
    language = "JavaScript"
    print("داخل تابع:", language)

change_language()
print("بیرون تابع:", language)

خروجی:

داخل تابع: JavaScript
بیرون تابع: Python

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

سایه‌انداختن متغیرها یا Variable Shadowing

وقتی یک نام محلی با نام موجود در محدوده بیرونی یکسان باشد، نام محلی در داخل آن محدوده اولویت دارد. این رفتار Shadowing نامیده می‌شود.

status = "عمومی"

def show_status():
    status = "محلی"
    print(status)

show_status()
print(status)

استفاده زیاد از نام‌های یکسان ممکن است خوانایی برنامه را کاهش دهد. بهتر است نام متغیرها نقش آن‌ها را واضح بیان کند.

چرا تغییر متغیر Global داخل تابع خطا می‌دهد؟

کد زیر در نگاه نخست منطقی به نظر می‌رسد:

score = 10

def increase_score():
    score = score + 1
    return score

print(increase_score())

اما پایتون خطای زیر را ایجاد می‌کند:

UnboundLocalError: local variable 'score' referenced before assignment

چون داخل تابع به score مقدار داده شده است، پایتون آن را محلی فرض می‌کند. در سمت راست عبارت، متغیر محلی هنوز مقداری ندارد.

کلمه کلیدی global در پایتون

با کلمه کلیدی global اعلام می‌کنیم که نام موردنظر به متغیر سراسری اشاره دارد:

score = 10

def increase_score():
    global score
    score = score + 1
    return score

print(increase_score())
print(score)

خروجی:

11
11

اکنون تابع مقدار متغیر سراسری را مستقیماً تغییر داده است.

ساخت متغیر Global از داخل تابع

def create_setting():
    global theme
    theme = "dark"

create_setting()
print(theme)

این روش ممکن است، اما معمولاً بهتر است متغیرهای سراسری در سطح اصلی فایل به‌صورت واضح تعریف شوند.

چرا استفاده زیاد از global مناسب نیست؟

متغیرهای سراسری قابل تغییر می‌توانند باعث شوند نتیجه یک تابع به وضعیت پنهان برنامه وابسته باشد. پیامدهای رایج:

  • تشخیص محل تغییر داده دشوار می‌شود.
  • آزمایش تابع سخت‌تر می‌شود.
  • توابع به بخش‌های دیگر برنامه وابسته می‌شوند.
  • احتمال ایجاد خطا در پروژه‌های بزرگ افزایش می‌یابد.

به‌جای تغییر مستقیم متغیر سراسری، اغلب بهتر است مقدار را به تابع بدهیم و نتیجه جدید را برگردانیم:

def increase_score(current_score):
    return current_score + 1

score = 10
score = increase_score(score)
print(score)

این تابع مستقل‌تر، قابل آزمایش‌تر و قابل استفاده مجدد است.

تغییر محتوای یک شیء سراسری

تفاوت مهمی میان اتصال دوباره یک نام و تغییر محتوای یک شیء قابل تغییر وجود دارد. تابع زیر بدون استفاده از global محتوای لیست سراسری را تغییر می‌دهد:

tasks = []

def add_task(task):
    tasks.append(task)

add_task("مطالعه پایتون")
print(tasks)

تابع نام tasks را به شیء دیگری متصل نکرده است؛ فقط محتوای همان لیست را تغییر داده است.

اما کد زیر اتصال نام را تغییر می‌دهد و بدون global یک متغیر محلی می‌سازد:

tasks = []

def reset_tasks():
    tasks = []
    return tasks

برای طراحی شفاف‌تر، بهتر است داده را به تابع ارسال و نتیجه را برگردانیم:

def add_task(task_list, task):
    task_list.append(task)
    return task_list

محدوده متغیر در شرط‌ها و حلقه‌ها

در پایتون، بلوک‌های if، for و while محدوده محلی جداگانه ایجاد نمی‌کنند:

if True:
    message = "شرط اجرا شد"

print(message)
for number in range(3):
    result = number * 2

print(number)
print(result)

نام‌های ساخته‌شده در این بلوک‌ها در محدوده اطراف قابل دسترسی‌اند. بااین‌حال، بهتر است به مقدار آخر متغیر حلقه وابسته نشویم مگر این رفتار عمداً موردنظر باشد.

محدوده توابع تو‌در‌تو

یک تابع می‌تواند داخل تابع دیگری تعریف شود:

def outer_function():
    message = "متغیر تابع بیرونی"

    def inner_function():
        print(message)

    inner_function()

outer_function()

تابع داخلی می‌تواند متغیر تابع بیرونی را بخواند. این محدوده، Enclosing نام دارد.

کلمه کلیدی nonlocal

اگر تابع داخلی بخواهد متغیر تابع بیرونی را تغییر دهد، از nonlocal استفاده می‌کنیم:

def create_counter():
    count = 0

    def increase():
        nonlocal count
        count += 1
        return count

    return increase

counter = create_counter()
print(counter())
print(counter())
print(counter())

خروجی:

1
2
3

nonlocal به نزدیک‌ترین متغیر موجود در محدوده تابع بیرونی اشاره می‌کند، نه محدوده سراسری.

تفاوت global و nonlocal

کلمه کلیدیمحدوده هدفکاربرد
globalسطح سراسری ماژولتغییر نامی که بیرون همه توابع تعریف شده است
nonlocalنزدیک‌ترین تابع بیرونیتغییر متغیر تابع محصورکننده در توابع تو‌در‌تو

قانون LEGB در پایتون

پایتون برای یافتن یک نام، محدوده‌ها را به ترتیب زیر بررسی می‌کند:

  1. Local: محدوده محلی تابع فعلی
  2. Enclosing: محدوده توابع بیرونی
  3. Global: سطح اصلی فایل یا ماژول
  4. Built-in: نام‌های داخلی پایتون مانند print و len
name = "Global"

def outer():
    name = "Enclosing"

    def inner():
        name = "Local"
        print(name)

    inner()

outer()

تابع داخلی ابتدا محدوده Local را بررسی می‌کند و مقدار "Local" را پیدا می‌کند.

نمونه جست‌وجوی مرحله‌ای LEGB

name = "Global"

def outer():
    name = "Enclosing"

    def inner():
        print(name)

    inner()

outer()

چون متغیر name در تابع داخلی وجود ندارد، پایتون آن را در محدوده Enclosing پیدا می‌کند.

محدوده Built-in

نام‌هایی مانند print، len، sum و list در محدوده داخلی پایتون قرار دارند.

numbers = [10, 20, 30]
print(len(numbers))

نباید بدون ضرورت از نام توابع داخلی برای متغیرهای خود استفاده کنیم:

list = [1, 2, 3]
new_list = list("Python")

در این حالت نام داخلی list با متغیر جدید پوشانده شده و فراخوانی دوم خطا ایجاد می‌کند.

خطای NameError

وقتی پایتون یک نام را در هیچ‌کدام از محدوده‌های LEGB پیدا نکند، خطای NameError ایجاد می‌شود:

print(user_name)

دلایل رایج:

  • متغیر هنوز تعریف نشده است.
  • نام متغیر اشتباه تایپ شده است.
  • متغیر در محدوده دیگری ساخته شده است.
  • تابع یا ماژول موردنظر وارد نشده است.

خطای UnboundLocalError

این خطا زمانی رخ می‌دهد که پایتون نامی را محلی تشخیص دهد، اما پیش از مقداردهی از آن استفاده شود:

balance = 1000

def withdraw(amount):
    balance = balance - amount
    return balance

راه‌های اصلاح:

  • مقدار را به‌عنوان آرگومان به تابع ارسال کنیم.
  • نتیجه را با return برگردانیم.
  • فقط در صورت نیاز واقعی از global استفاده کنیم.
def withdraw(current_balance, amount):
    if amount > current_balance:
        return current_balance, "موجودی کافی نیست"

    return current_balance - amount, "برداشت انجام شد"

balance = 1000
balance, message = withdraw(balance, 300)
print(balance, message)

مشاهده محدوده با locals و globals

تابع locals() دیکشنری نام‌های محدوده محلی فعلی و تابع globals() دیکشنری نام‌های سراسری ماژول را برمی‌گرداند:

course = "Python"

def show_scope():
    lesson = 6
    print(locals())

show_scope()
print("course" in globals())

این ابزارها بیشتر برای بررسی و اشکال‌زدایی مفیدند. تغییر مستقیم خروجی آن‌ها برای مدیریت عادی متغیرها توصیه نمی‌شود.

ثابت‌های سراسری

برای مقدارهایی که باید در سراسر برنامه خوانده شوند و تغییر نکنند، استفاده از ثابت‌های سراسری قابل قبول است. طبق قرارداد، نام ثابت با حروف بزرگ نوشته می‌شود:

MAX_LOGIN_ATTEMPTS = 3
TAX_RATE = 0.09
APP_NAME = "Dev Source"

def calculate_tax(price):
    return price * TAX_RATE

حروف بزرگ مانع تغییر مقدار نمی‌شوند؛ فقط به برنامه‌نویسان اعلام می‌کنند که این مقدار باید ثابت در نظر گرفته شود.

توابع خالص و مدیریت بهتر Scope

تابع خالص معمولاً فقط از ورودی‌های خود استفاده می‌کند و بدون تغییر وضعیت بیرونی، نتیجه‌ای برمی‌گرداند:

def calculate_final_price(price, discount_percent, tax_rate):
    discounted = price - (price * discount_percent / 100)
    return discounted + (discounted * tax_rate)

مزایای این سبک:

  • نتیجه تابع قابل پیش‌بینی است.
  • آزمایش تابع آسان‌تر است.
  • وابستگی به متغیرهای سراسری کاهش می‌یابد.
  • استفاده دوباره از تابع ساده‌تر می‌شود.

پروژه اول: مدیریت موجودی حساب بدون global

در این پروژه، موجودی حساب به تابع‌ها ارسال و نتیجه جدید برگردانده می‌شود:

def deposit(balance, amount):
    if amount <= 0:
        return balance, "مبلغ واریز نامعتبر است"

    return balance + amount, "واریز با موفقیت انجام شد"


def withdraw(balance, amount):
    if amount <= 0:
        return balance, "مبلغ برداشت نامعتبر است"

    if amount > balance:
        return balance, "موجودی کافی نیست"

    return balance - amount, "برداشت با موفقیت انجام شد"


account_balance = 1000000

account_balance, message = deposit(account_balance, 250000)
print(message)
print("موجودی:", account_balance)

account_balance, message = withdraw(account_balance, 400000)
print(message)
print("موجودی:", account_balance)

هیچ تابعی مستقیماً متغیر سراسری را تغییر نمی‌دهد. مقدار جدید به‌شکل واضح به متغیر اصلی اختصاص داده می‌شود.

پروژه دوم: شمارنده با nonlocal

این پروژه یک تابع سازنده شمارنده ایجاد می‌کند:

def create_counter(start=0, step=1):
    count = start

    def increase():
        nonlocal count
        count += step
        return count

    def reset():
        nonlocal count
        count = start
        return count

    return increase, reset


increase_counter, reset_counter = create_counter(start=10, step=5)

print(increase_counter())
print(increase_counter())
print(reset_counter())
print(increase_counter())

متغیر count نه محلی تابع داخلی است و نه سراسری؛ بلکه در محدوده تابع create_counter قرار دارد و با nonlocal تغییر می‌کند.

خطاها و اشتباهات رایج

۱. انتظار دسترسی به متغیر Local بیرون تابع

def calculate():
    result = 20

calculate()
print(result)

راه‌حل: مقدار را با return برگردانید.

۲. تغییر متغیر Global بدون global

count = 0

def increase():
    count += 1

راه‌حل بهتر: مقدار را به تابع ارسال کنید و نتیجه را برگردانید.

۳. استفاده زیاد از global

استفاده گسترده از global وابستگی پنهان ایجاد می‌کند. ورودی و خروجی توابع را صریح نگه دارید.

۴. پوشاندن نام توابع داخلی

sum = 100
print(sum([1, 2, 3]))

نام‌هایی مانند sum، list، str و input را برای متغیرهای شخصی انتخاب نکنید.

۵. اشتباه‌گرفتن global و nonlocal

global به سطح ماژول و nonlocal به نزدیک‌ترین تابع بیرونی اشاره می‌کند.

۶. تکیه بر متغیر حلقه پس از پایان حلقه

متغیر حلقه در پایتون باقی می‌ماند، اما وابستگی غیرضروری به آخرین مقدار آن می‌تواند کد را شکننده کند.

بهترین روش‌های مدیریت محدوده متغیرها

  • متغیرها را تا حد امکان در کوچک‌ترین محدوده لازم تعریف کنید.
  • داده موردنیاز تابع را از طریق پارامترها ارسال کنید.
  • نتیجه را با return برگردانید.
  • از متغیرهای سراسری قابل تغییر تا حد امکان دوری کنید.
  • برای ثابت‌های سراسری از نام‌های حروف بزرگ استفاده کنید.
  • نام توابع داخلی پایتون را نپوشانید.
  • از global و nonlocal فقط زمانی استفاده کنید که طراحی برنامه واقعاً به آن نیاز دارد.
  • برای متغیرهای محلی و سراسری نام‌های واضح و متفاوت انتخاب کنید.

تمرین‌های درس

  1. تابعی بنویسید که یک متغیر محلی بسازد و مقدار آن را برگرداند.
  2. یک متغیر سراسری تعریف کنید و آن را بدون تغییر داخل تابع نمایش دهید.
  3. نمونه‌ای بسازید که متغیر محلی، نام سراسری مشابه را Shadow کند.
  4. کدی که خطای UnboundLocalError ایجاد می‌کند بنویسید و سپس آن را بدون global اصلاح کنید.
  5. تابع افزایش امتیاز را به‌صورت ورودی–خروجی طراحی کنید.
  6. یک ثابت سراسری برای نرخ مالیات بسازید و در تابع محاسبه مالیات استفاده کنید.
  7. تابع تو‌در‌تویی بنویسید که مقدار تابع بیرونی را بخواند.
  8. با nonlocal یک شمارنده کاهشی بسازید.
  9. مثالی برای چهار مرحله قانون LEGB آماده کنید.
  10. یک برنامه کوچک مدیریت سبد خرید بسازید که بدون متغیر سراسری قابل تغییر کار کند.

📌 همچنین بخوانید: بازار کار پایتون در ایران، دستورهای break، continue و pass در پایتون و ساخت ربات تلگرام با پایتون

پرسش‌های متداول

آیا متغیر Global داخل همه فایل‌های پروژه قابل دسترسی است؟

خیر. متغیر Global در سطح همان ماژول یا فایل تعریف می‌شود. برای استفاده در فایل دیگر باید ماژول را وارد کرد و از نام آن استفاده کرد.

آیا برای خواندن متغیر سراسری به global نیاز داریم؟

خیر. global زمانی لازم است که بخواهیم داخل تابع نام سراسری را دوباره مقداردهی کنیم.

آیا if و for محدوده جداگانه می‌سازند؟

خیر. در پایتون، بلوک‌های شرط و حلقه محدوده تابع‌مانند مستقل ایجاد نمی‌کنند.

تفاوت NameError و UnboundLocalError چیست؟

NameError زمانی رخ می‌دهد که نامی پیدا نشود. UnboundLocalError زمانی رخ می‌دهد که نام محلی تشخیص داده شده، اما پیش از مقداردهی استفاده شود.

آیا استفاده از global همیشه اشتباه است؟

خیر، اما باید محدود و آگاهانه باشد. برای ثابت‌ها، تنظیمات ساده یا برخی وضعیت‌های سطح برنامه ممکن است مناسب باشد؛ بااین‌حال تغییر گسترده متغیرهای سراسری نگهداری برنامه را دشوار می‌کند.

nonlocal کجا استفاده می‌شود؟

داخل تابع تو‌در‌تو، زمانی که لازم است متغیر تابع بیرونی تغییر کند.

جمع‌بندی

متغیرهای محلی فقط در تابع خود قابل استفاده‌اند و متغیرهای سراسری در سطح ماژول تعریف می‌شوند. پایتون نام‌ها را براساس قانون LEGB جست‌وجو می‌کند. برای تغییر متغیر سراسری از global و برای تغییر متغیر تابع بیرونی از nonlocal استفاده می‌شود؛ اما در بیشتر برنامه‌ها بهتر است داده‌ها به‌صورت پارامتر وارد تابع شوند و نتیجه با return بازگردد.

درس بعدی: آرگومان‌های *args و **kwargs در پایتون — در حال آماده‌سازی.

ادامه مسیر یادگیری پایتون

برای یادگیری منظم، آموزش‌های مرتبط زیر را به‌ترتیب دنبال کنید:

می‌خواهی پیشرفتت ذخیره شود؟با ورود یا ثبت‌نام رایگان، درس‌های تمام‌شده را علامت بزن و درصد پیشرفتت را در حساب کاربری ببین.ورود / ثبت‌نام برای ذخیره پیشرفت
درس بعدی · دوره پایتون متوسط (7 از 18)آرگومان‌های *args و **kwargs در پایتون ←
آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.
🧭 مسیر یادگیری پایتون از صفر
ماژول 6: توابع — درس 26 از 44

برای ذخیره پیشرفت در همه دستگاه‌ها و دریافت گواهی پایان مسیر، رایگان ثبت‌نام کن یا وارد شو.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *