سرفصل‌های آموزشی
آموزش پایتون
آشنایی با مفهوم Scope در زبان برنامه‌نویسی پایتون

آشنایی با مفهوم Scope در زبان برنامه‌نویسی پایتون

هر یک از پارامترهای ورودی و متغیرهایی که در زمان تعریف یک فانکشن در بدنۀ آن استفاده می‌شوند، در اصطلاحاً Local Scope (دامنۀ محلی) آن قرار دارند و تمامی متغیرهایی که در خارج از بدنۀ هر یک از فانکشن‌ها ‌تعریف می‌شوند متعلق به Global Scope (دامنۀ سراسری) اپلیکیشن می‌باشند. متغیرهایی که در هر یک از اسکوپ‌های لوکال و گلوبال تعریف می‌شوند، به ترتیب «متغیر لوکال» و «متغیر گلوبال» نامیده می‌شوند. همچنین لازم به یادآوری است که یک متغیر را نمی‌توان به صورت هم‌زمان در هر دو اسکوپ لوکال و گلوبال تعریف کرده و مورد استفاده قرار داد؛ به عبارت دیگر، یک متغیر یا لوکال است و یا به عنوان متغیری گلوبال شناخته می‌شود.

در واقع، می‌توان اسکوپ متعلق به هر یک از متغیرها را مانند ظرفی در نظر گرفت که وقتی این ظرف از بین برود، دیگر هیچ اثری از محتویات داخل آن باقی نمی‌ماند. به عبارتی، با شروع برنامه تنها یک اسکوپ گلوبال ایجاد می‌شود و زمانی که برنامۀ مذکور به پایان می‌رسد اسکوپ گلوبال نیز از بین می‌رود و از همین روی تمامی متغیرهای متعلق به این اسکوپ نیز از حافظۀ کامپیوتر پاک می‌شوند (اگر فرض کنیم که متغیرهای اسکوپ گلوبال پس از پایان یافتن برنامه از حافظۀ کامپیوتر پاک نشوند، در صورت اجرای مجدد برنامه متغیرهای مذکور آبجکت‌هایی که در آخرین اجرای برنامه به هر یک از آن‌ها منتسب شده بودند را به خاطر می‌آوردند که مسلماً چنین چیزی مطلوب نیست.)

اسکوپ لوکال نیز زمانی ایجاد می‌شود که فانکشنی را فراخوانی می‌کنیم و متغیرهای تعریف‌شده در این فانکشن نیز در اسکوپ لوکال آن قرار می‌گیرند و زمانی که فانکشن مذکور مقداری را برمی‌گرداند (آبجکتی از نوع None و یا هر آبجکت دیگری)، اسکوپ لوکال نیز از بین می‌رود که در صورت فراخوانی مجدد فانکشن، متغیرهای لوکال مقادیر اختصاص‌یافته در نوبت قبلی فراخوانی را به خاطر نخواهند داشت.

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

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

def setName():
    name = "Narges"
setName()
print(name)

همان‌طور که می‌بیند، فانکشنی به نام ()setName تعریف کرده‌ایم که در بدنۀ آن یک متغیر لوکال تحت عنوان name وجود دارد و گفته‌ایم در صورت فراخوانی استرینگ «Narges» به متغیر name منتسب شود. در ادامه فانکشن ()print را در اسکوپ گلوبال و خارج از فانکشن ()setName فراخوانی کرده و متغیر لوکالِ name را به عنوان آرگومان ورودی به آن می‌دهیم. حال کد فوق را در فایلی به نام setName.py ذخیره کرده و آن را اجرا می‌کنیم که خروجی حاصل از آن به صورت زیر می‌باشد:

Traceback (most recent call last):
  File " D:/sokanacademy/Python/ setName.py", line 5, in 
    print(name)
NameError: name 'name' is not defined

همان‌طور که می‌بینید، از اسکوپ گلوبال به متغیر لوکالِ name دسترسی نداریم و فراخوانی فانکشن ()print با آرگومان ورودی name منجر به ایجاد ارور می‌شود که به منظور رفع مشکل مذکور کافی است تا فانکشن ()print را به داخل بدنۀ فانکشن ()setName منتقل کنیم.

در قانون دوم به بیان این موضوع پرداخته شده است که دسترسی به متغیرهای گلوبال در اسکوپ لوکال امکان‌پذیر می‌باشد که برای درک بهتر این قانون کد زیر را مد نظر قرار می‌دهیم:

name = "Narges"
def printName():
    print(name)

printName()

در کد فوق، ابتدا متغیری گلوبال تحت عنوان name تعریف کرده و استرینگ «Narges» را به آن اختصاص می‌دهیم و در ادامه فانکشنی به نام ()printName تعریف کرده‌ایم که در آن گفته‌ایم در صورت فراخوانی، استرینگ منتسب به متغیر گلوبالِ name را در خروجی چاپ کند. سپس کد فوق را در فایلی به نام printName.py ذخیره کرده و آن را اجرا می‌کنیم که خروجی حاصل از اجرای آن به صورت زیر است:

Narges

همان‌طور که می‌بینید، از داخل اسکوپ لوکالِ فانکشن ()printName به مقدار متغیر گلوبالِ name دسترسی داریم و آن را به عنوان آرگومان ورودی به فانکشن ()print داده‌ایم که در نهایت استرینگ منتسب به این متغیر در خروجی نمایش داده می‌شود.

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

def setName():
    name = "Narges"
    
def getName():
    name = "Puya"
    setName()
    return name

print(getName())

در کد فوق، فانکشنی به نام ()setName تعریف کرده‌ایم که در صورت فراخوانی، استرینگ «Narges» را به متغیر لوکالِ name منتسب کرده و در سطر بعد فانکشنی دیگر تحت عنوان ()getName تعریف کرده‌ایم که در آن گفته‌ایم در صورت فراخوانی، استرینگ «Puya» به متغیر لوکالِ name منتسب شده و در ادامه متد ()setName فراخوانی شود و در نهایت استرینگ منتسب به متغیر لوکالِ name در خروجی ریترن شود و در سطر آخر نیز متد ()getName را فراخوانی کرده و خروجی حاصل از اجرای آن را به فانکشن ()print داده‌ایم.

حال برنامۀ فوق را در فایلی به نام getName.py ذخیره کرده و آن را اجرا می‌کنیم. اجرای این کد از فانکشن ()print در اسکوپ گلوبال برنامه آغاز می‌شود و در ادامه فانکشن ()getName به عنوان آرگومان ورودی فانکشن ()print فراخوانی می‌شود که فراخوانی این فانکشن منجر به ایجاد یک اسکوپ لوکال برای آن شده و دستورات بدنۀ فانکشن مذکور به ترتیب اجرا می‌شوند به طوری که استرینگ «Puya» به متغیر لوکالِ name منتسب شده و فانکشن ()setName فراخوانی می‌شود. فراخوانی فانکشن ()setName منجر به ایجاد یک اسکوپ لوکال برای این فانکشن می‌شود که اکنون دو اسکوپ لوکال داریم که هر یک از آن‌ها با فراخوانی فانکشن متناظرشان ایجاد شده‌اند.

با فراخوانی فانکشن ()setName تنها دستور قرار گرفته در بدنۀ آن اجرا می‌شود که بدین ترتیب یک متغیر لوکال به نام name ایجاد شده و استرینگ «Narges» به آن منتسب می‌شود و در ادامه آبجکتی از نوع None ریترن شده و کار فانکشن مذکور به پایان می‌رسد. حال در این شرایط اسکوپ لوکال ایجادشده برای فانکشن ()setName نیز از بین می‌رود و بدین ترتیب متغیر لوکالِ name مربوط به این فانکشن از حافظۀ کامپیوتر پاک می‌شود.

در ادامه مفسر پایتون وارد اسکوپ لوکال فانکشن ()getName شده و یک متغیر لوکال به نام name ساخته و استرینگ «Puya» را به آن منتسب می‌کند که با اجرای آخرین دستور از این فانکشن استرینگ منتسب به متغیر لوکال name ریترن شده و به فانکشن ()print داده می‌شود تا آن را در خروجی چاپ کند. در نهایت، خروجی حاصل از اجرای کد فوق بدین ترتیب خواهد بود:

Puya

همان‌طور که می‌بینید، دستور return در فانکشن ()getName متغیر لوکالِ name مربوط به اسکوپ لوکال این فانکشن را در خروجی ریترن می‌کند. در واقع، فانکشن ()setName متغیری لوکال با نام مشترکِ name دارد که با فراخوانی این فانکشن استرینگ «Narges» به این متغیر منتسب می‌شود اما این در حالی است که با پایان کار فانکشن ()setName متغیر لوکالِ name مربوط به آن نیز فراموش می‌شود. این موضوع در مورد فانکشن ()getName نیز صادق است به طوری که با پایان یافتن اجرای دستورات، اسکوپ لوکال مربوطه نیز از بین رفته و متغیرهای لوکال آن از حافظۀ سیستم پاک می‌شوند.

قانون چهارم نیز به بیان این موضوع اختصاص دارد که در اسکوپ لوکال نمی‌توان یک شناسۀ یکسان برای نام‌گذاری دو متغیر لوکال استفاده کرد اما این در حالی است که استفاده از شناسۀ مشابه به منظور نام‌گذاری متغیرهایی که یکی از آن‌ها متعلق به اسکوپ لوکال بوده و دیگری در اسکوپ گلوبال تعریف شده است، مانعی ندارد. همان‌طور که دیدیم، در مثال قبل یک متغیر لوکال با نام name و یک متغیر گلوبال هم با همین نام در برنامۀ خود داشتیم که به منظور جلوگیری از اشتباه و سردرگمی، بهتر است تا حد امکان از شناسه‌های یکسان حتی در اسکوپ‌های متفاوت استفاده نکنیم.

در واقع، اهمیت اسکوپ‌های لوکال در این است که اگر به دلایلی همچون مقداردهی نادرست متغیرهای لوکال خطایی در برنامه پیش آید، به راحتی می‌توانیم متغیری که منجر به بروز ارور شده را بیابیم اما این در حالی است که در مورد متغیرهای گلوبال به سختی می‌توانیم بفهمیم متغیر مد نظر در کدام قسمت از کدها به صورت نادرست مقداردهی شده است چرا که تمام واحدهای برنامه می‌توانند روی آن تأثیرگذار باشند!

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

online-support-icon