مباحث تکمیلی متغیرها در پایتون

زمان لازم برای خواندن: حدود ۱۲ دقیقه
متغیرهای پایتون

در این نوشته به سراغ مباحث تکمیلی متغیرهای پایتون نظیر پویایی تایپ متغیرها، تایپ هینت، اسکوپ‌های متغیرها و روش‌های تکمیلی برای ایجاد متغیرها در پایتون خواهیم رفت.

در قسمت اول از مجموعه نوشته‌های متغیرها در پایتون، به معرفی مفهوم متغیر و شیوه استفاده از آن پرداخته شد. در قسمت دوم از این مجموعه، سعی شد قواعد و الزامات نامگذاری متغیرها در پایتون را به همراه best practice های نامگذاری متغیرها بررسی کنیم. در این قسمت، با مفاهیمی نظیر پویایی تایپ متغیرها، تایپ هینت، اسکوپ‌ها و روش‌های تکمیلی برای ایجاد متغیرها در پایتون آشنا خواهیم شد.

فهرست مطالب

ارتباط متغیرها و آبجکت‌ها

وقتی که با استفاده از عملگر = یک متغیر ایجاد می‌کنید، چه اتفاقی رخ می‌دهد؟ این سوال در پایتون از اهمیت بالایی برخوردار است زیرا رفتار پایتون در این زمینه، با خیلی از زبان‌های برنامه‌نویسی متفاوت است.

پایتون یک زبان برنامه‌نویسی شیء‌گرا (object-oriented programming) است. هر داده‌ای در یک برنامه پایتونی، یک شیء یا آبجکت مشخص است. مثلاً:

>>> 300
300

>>> type(300)
<class 'int'>

در این مثال، تشخیص نوع با تابع type در پایتون انجام شده است و همانطور که می‌بینید، پایتون حتی برای عدد ۳۰۰ که به تنهایی وارد شده است یک آبجکت عدد صحیح (int) می‌سازد و مقدار ۳۰۰ را به آن اختصاص می‌دهد. یک متغیر در پایتون، نامی نمادین برای اشاره یا point کردن به یک آبجکت است. وقتی که یک آبجکت به یک متغیر اختصاص داده می‌شود، شما می‌توانید با استفاده از نام متغیر به آن اشاره کنید ولی خود داده درون آبجکت قرار دارد.

به عنوان مثال، متغیر فرضی n را در نظر بگیرید:

>>> n = 300

در اینجا یک آبجکت عدد صحیح با مقدار ۳۰۰ ایجاد می‌شود و متغیر n به آن آبجکت اشاره می‌کند:

تصویر متغیرها و آبجکت‌ها

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

>>> n = 300
>>> id(n)
4399012816

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

شما می‌توانید چندین متغیر بسازید که همگی به یک آبجکت اشاره دارند و چون آبجکت بین این متغیرها مشترک است، شناسه یکتای آنها نیز با هم برابر است:

>>> m = n
>>> id(n) == id(m)
True

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

تصویر متغیرها و آبجکت‌ها

حالا فرض کنید که شما مقدار متغیر m را به ۴۰۰ تغیر می‌دهید:

تصویر متغیرها و آبجکت‌ها

در اینجا پایتون یک آبجکت جدید با مقدار ۴۰۰ می‌سازد و متغیر m به آن اشاره خواهد کرد.

در نهایت، اگر شما مقدار متغیر n را نیز تغییر دهید، آبجکت عدد صحیح ۳۰۰ دیگر توسط هیچ متغیری استفاده نمی‌شود. در این حالت، این آبجکت یتیم می‌شود و پایتون فضای حافظه مربوط به آن را آزاد می‌کند تا برای کارهای دیگری از آن استفاده کند. در واژه‌شناسی برنامه‌نویسی، به این فرآیند اصطلاحاً garbage collection گفته می‌شود.

پویایی تایپ متغیرها

در بسیاری از زبان‌های برنامه‌نویسی، متغیرها اصطلاحاً statically typed هستند؛ به این معنی که در ابتدای تعریف آنها، مشخص می‌شود این متغیر چه تایپی دارد و تا پایان طول عمر خود، همان تایپ را خواهد داشت. هر مقداری که به آن متغیر اختصاص یابد، صرفاً باید از همان تایپ باشد.

همانطور که قبلتر نیز اشاره شد، متغیرهای پایتون dynamically typed هستند! در پایتون شما می توانید مقادیری با تایپ متفاوت را به یک متغیر اختصاص دهید:

>>> value = "A string value"
>>> value
'A string value'

>>> # Later
>>> value = 23.5
>>> value
23.5

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

>>> value = "A string value"
>>> value.upper()
'A STRING VALUE'

>>> # Later
>>> value = 23.5

>>> # Try to use .upper() again
>>> value.upper()
Traceback (most recent call last):
    ...
AttributeError: 'float' object has no attribute 'upper'

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

تایپ هینت (Type Hint)

از نسخه 3.5 پایتون به بعد، یک ویژگی جالب به نام Type Hints به هسته پایتون افزوده شد. سینتکس کلی افزودن تایپ هینت به صورت زیر است:

variable: data_type [= value]

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

>>> number: int

>>> number
Traceback (most recent call last):
    ...
NameError: name 'number' is not defined

برای تایپ‌های ساده‌ای مانند استرینگ و عدد صحیح و اعشاری، شاید تایپ هینت اضافی به نظر برسد:

>>> language: str = "Python"
>>> number: int = 42
>>> coefficient: float = 2.87

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

>>> colors = {
...     "red": "#FF0000",
...     "green": "#00FF00",
...     "blue": "#0000FF",
...     "yellow": "#FFFF00",
...     "black": "#000000",
...     "white": "#FFFFFF",
... }

در این مثال، اگر تایپ هینت را برای دیکشنری colors تعریف کنید، اطلاعات مفیدی به کاربران کدهایتان می‌دهید:

>>> colors: dict[str, str] = {
...     "red": "#FF0000",
...     "green": "#00FF00",
...     "blue": "#0000FF",
...     "yellow": "#FFFF00",
...     "black": "#000000",
...     "white": "#FFFFFF",
... }

به این ترتیب، مشخص می‌شود که متغیر colors یک دیکشنری است که کلیدها و مقادیر آنها از نوع استرینگ هستند. حالا فرض کنید دیکشنری دیگری با همین نام دارید که کد رنگ‌ها را به صورت rgb ذخیره کرده است:

>>> colors = {
...     "Red": (255, 0, 0),
...     "Green": (0, 255, 0),
...     "Blue": (0, 0, 255),
...     "Yellow": (255, 255, 0),
...     "Black": (0, 0, 0),
...     "White": (255, 255, 255),
... }

در ادامه استفاده از کدهایتان، ممکن است در نقطه‌ای نتوانید بین این دو دیکشنری تمایز قائل شوید. اما با تعریف تایپ هینت برای دیکشنری دوم، مشکل حل می‌شود:

>>> colors: dict[str, tuple[int, int, int]] = {
...     "Red": (255, 0, 0),
...     "Green": (0, 255, 0),
...     "Blue": (0, 0, 255),
...     "Yellow": (255, 255, 0),
...     "Black": (0, 0, 0),
...     "White": (255, 255, 255),
... }

با این کار، اگر داده‌ای با تایپ مشخص شده را وارد نکنید linter ادیتور به شما پیغام خطا می‌دهد.

آشنایی با تابع isinstance

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

سایر روش‌های ایجاد متغیرها

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

۱) تخصیص موازی

شما می‌توانید چندین متغیر را همزمان در یک خط کد تعریف کنید. به این کار، تخصیص موازی یا Parallel Assignment می‌گویند:

>>> is_authenticated = is_active = is_admin = False

>>> is_authenticated
False
>>> is_active
False
>>> is_admin
False

۲) باز کردن تاپل‌ها

یکی از راه‌های جالب تعریف چندین متغیر در یک خط، استفاده از قابلیت Iterable Unpacking است که خیلی اوقات از آن به عنوان باز کردن تاپل یا Tuple Unpacking نیز یاد می‌شود. به صورت کلی، Iterable Unpacking منجر به توزیع اجزای یک Iterable (مثل لیست یا تاپل) بین چندین متغیر می‌شود. به عنوان مثال، فرض کنید که اطلاعات هویتی یک فرد را درون یک تاپل ذخیره کرده‌اید:

>>> person = ("Jane", 25, "Python Dev")

اگر شما با مفهوم آنپکینگ تاپل آشنا نباشید، می‌بایست هر بخش از اطلاعات را جداگانه از متغیر person بیرون بکشید و درون یک متغیر جدید ذخیره کنید:

>>> name = person[0]
>>> age = person[1]
>>> job = person[2]

>>> name
'Jane'
>>> age
25
>>> job
'Python Dev'

اما با استفاده از مفهوم آنپکینگ تاپل‌ها، این کار با یک خط کد و به سادگی انجام می‌شود:

>>> name, age, job = person

>>> name
'Jane'
>>> age
25
>>> job
'Python Dev'

همچنین با استفاده از علامت ستاره (*) می‌توانید چندین بخش از متغیر را درون یک لیست ذخیره کنید:

>>> person = ("Jane", "Doe", 25, "Python Dev")
>>> *fullname, age, job = person
>>> fullname
['Jane', 'Doe']
>>> age
25
>>> job
'Python Dev'

در این مثال، اجزای آخر و یکی مانده به آخر متغیر person درون متغیرهای age و job ذخیره می‌شوند و باقیمانده اجزای آن درون متغیری به نام fullname ذخیره خواهند شد.

یکی از بهترین کاربردهای آنپکینگ، جابجایی مقادیر ۲ متغیر است:

>>> a = 5
>>> b = 10

>>> a, b = b, a

>>> a
10
>>> b
5

در این روش، بدون نیاز به یک متغیر موقت، مقادیر دو متغیر با هم عوض می‌شوند.

۳) عملگر Walrus

یکی دیگر از راه‌های ایجاد متغیرهای پایتون، استفاده از عملگر Walrus است. این عملگر که ترکیبی از دو کاراکتر : و = است، عملیات تعریف متغیر و فراخوانی آن را همزمان در یک خط انجام می‌دهد:

>>> (walrus := True)
True

>>> walrus
True

دلیل انتخاب این نام، شباهت ظاهری این عملگر به چشم‌ها و دندان‌های شیر دریایی (به انگلیسی Walrus) است.

عملگر Walrus باعث می‌شود تعریف و تخصیص متغیر در یک خط انجام شود که منجر به کوتاه شدن کد و بهبود خوانایی آن می‌شود. به عنوان مثال:

>>> while (line := input("Type some text: ")) != "stop":
...     print(line)
...
Type some text: Python
Python
Type some text: Walrus
Walrus
Type some text: stop

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

۴) تطبیق الگو

یکی دیگر از روش‌های ایجاد متغیرها در پایتون، استفاده از تطبیق الگوی ساختاری یا Structural pattern matching است. در این روش، هرگاه الگویی با شرایط تعریف شده یافته شود، پایتون مقادیر دریافتی را درون متغیرهای جدیدی ذخیره می‌کند. مثلاً:

>>> command = ("move", 10, 25)

>>> match command:
...     case ("move", x, y):
...         print(f"Moving to ({x}, {y})")
...
Moving to (10, 25)

>>> x
10
>>> y
25

همانطور که می‌بینید متغیرهای x و y قبل از عبارت match وجود ندارند اما هنگامی که الگویی به case داده می‌شود، پارامترهای دوم و سوم تاپل command به صورت دو متغیر جدید تعریف می‌شوند.

اسکوپ‌های متغیرها

مفهوم دامنه (Scope) متغیرها، نحوه جستجوی متغیرها و نام‌ها در کد شما را مشخص می‌کند. اسکوپ یک متغیر وابسته به این است که آن متغیر در کجای کد شما تعریف شده است.

در پایتون ۴ نوع اسکوپ متفاوت می‌توان تعریف کرد که به صورت اختصاری LEGB نامیده می‌شوند. این حروف به ترتیب حرف اول کلمات local و enclosing و global و built-in هستند.

۱) اسکوپ عمومی

متغیرهای اسکوپ عمومی (global) آن دسته از متغیرها هستند که در سطح ماژول ایجاد می‌شوند. این متغیرها درون همان ماژول و سایر ماژول‌ها و فایل‌هایی که ماژول والد را ایمپورت کرده‌اند، در دسترس خواهند بود.

به عنوان مثال، فرض کنید که یک ماژول config.py دارید:

# config.py file
settings = {
    "background": "black",
    "foreground": "white",
    "logo": "logo.png",
    "font": "Helvetica",
}

این فایل، حاوی یک متغیر دیکشنری به نام settings است که برخی پارامترهای تنظیمات را برای یک برنامه فرضی ذخیره کرده است. اگر شما متغیر settings را درون یک فایل دیگر ایمپورت کنید، این متغیر به اسکوپ این فایل جدید نیز وارد می‌شود و در کل کدهای فایل جدید، در دسترس قرار خواهد گرفت:

>>> from config import settings

>>> settings["background"]
'black'
>>> settings["font"]
'Helvetica'

۲) اسکوپ محلی

متغیرهای اسکوپ محلی (local) آن دسته از متغیرها هستند که درون یک تابع تعریف می‌شوند. این متغیرها می‌توانند نتایج محاسبات میانی را برای شما نگهداری کنند و یکی از اهداف آنها، کمک به خوانایی کدهاست. به عنوان مثال:

>>> def function():
...     age = 42
...     return age
...

>>> function()
42

>>> age
Traceback (most recent call last):
    ...
NameError: name 'age' is not defined

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

۳) اسکوپ غیرمحلی

اگر شما یک تابع را درون یک تابع دیگر تعریف کنید، با مفهومی به نام متغیرهای اسکوپ غیرمحلی (non-local) مواجه خواهید شد. مثال زیر، مفاهیم متغیرهای عمومی، محلی و غیرمحلی را به خوبی نمایش می‌دهد:

# Global scope
global_variable = "global"

def outer_func():
    # Nonlocal scope
    nonlocal_variable = "nonlocal"
    def inner_func():
        # Local scope
        local_variable = "local"
        print(f"Hi from the '{local_variable}' scope!")
        print(f"Hi from the '{nonlocal_variable}' scope!")
        print(f"Hi from the '{global_variable}' scope!")
    inner_func()

در این مثال، یک متغیر با اسکوپ عمومی به نام global_variable ایجاد شده است. درون تابع بیرونی با نام ()outer_func، یک متغیر به نام nonlocal_variable و یک تابع داخلی به نام ()inner_func تعریف شده است. متغیر nonlocal_variable نسبت به تابع بیرونی، یک متغیر محلی است اما نسبت به تابع داخلی، یک متغیر غیرمحلی به حساب می‌آید.

به صورت کلی:

  • متغیرهای عمومی در کل فضای کد قابل دسترسی هستند.
  • متغیرهای محلی فقط درون تابع والد خود در دسترس هستند.
  • متغیرهای غیرمحلی هم داخل تابع والد و هم داخل توابع فرزند قابل دسترسی هستند.

نکته: به متغیرهای غیرمحلی، متغیرهای enclosing نیز گفته می‌شود.

۴) اسکوپ پیش‌ساخته

آن دسته از متغیرها و اسامی که از قبل توسط پایتون ایجاد شده‌اند و در اختیار پایتون هستند، متغیرهای اسکوپ پیش‌ساخته (built-in) را تشکیل می‌دهند.

مثلاً نام‌های len و str در کد زیر، به اسکوپ پیش‌ساخته تعلق دارند:

def my_func():
    items = [1, 2, 3]
    length = len(items)  # 'len' is from built-in scope
    return str(length)   # 'str' is from built-in scope

آخرین به‌روزرسانی: ۳۰ مهر ۱۴۰۵ | برچسب‌ها: - - - -

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

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

مشاوره رایگان

علاقمند به یادگیری طراحی سایت یا دنبال مشاوره در این زمینه هستی؟