بک اندپایتون

درک متغیرهای کلاس و Instance در پایتون ۳

مقدمه

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

متغیر کلاس (Class Variable) داخل کلاس اما خارج از هر متدی تعریف می‌شود و توسط هر Instance آن کلاس به اشتراک گذاشته می‌شود. متغیر Instance (Instance Variable) داخل متدی (معمولاً Constructor یعنی __init__) با استفاده از self تعریف می‌شود و هر شیء نسخه جداگانه خودش را می‌گیرد. به‌طور خلاصه، متغیرهای کلاس داده‌ای را نگه می‌دارند که به کلاس به‌عنوان یک کل تعلق دارد؛ در حالی که متغیرهای Instance داده‌ای را نگه می‌دارند که به یک شیء خاص تعلق دارد.

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

یکی از اصول توسعه نرم‌افزار، اصل DRY است که مخفف «خودت را تکرار نکن» (Don’t Repeat Yourself) است. این اصل به سمت محدود کردن تکرار درون کد جهت دارد؛ و برنامه‌نویسی شیءگرا به اصل DRY پایبند است؛ چون افزونگی را کاهش می‌دهد.

این tutorial استفاده از هر دو متغیر کلاس و Instance را در برنامه‌نویسی شیءگرا درون پایتون نشان می‌دهد. متغیرهای کلاس و Instance را تعریف می‌کنید؛ می‌بینید پایتون چگونه وقتی نامی در هر دو مکان وجود دارد تصمیم می‌گیرد کدام را بخواند؛ یاد می‌گیرید چطور از باگ رایج متغیر کلاسِ Mutable اجتناب کنید؛ این دو نوع متغیر را کنار هم مقایسه می‌کنید؛ می‌بینید تحت وراثت چگونه رفتار می‌کنند؛ و با __dict__ و vars() بازرسی‌شان می‌کنید. مثال‌ها با پایتون 3.14—نسخه پایدار فعلی—تست شده‌اند؛ اما رفتار توصیف‌شده برای هر نسخه مدرن پایتون ۳ اعمال می‌شود.

نکات کلیدی

  • متغیر کلاس مستقیماً در بدنه کلاس تعریف شده و توسط همه Instanceهای آن کلاس به اشتراک گذاشته می‌شود.
  • متغیر Instance روی self (معمولاً داخل __init__) تعریف شده و برای هر شیء یکتاست.
  • وقتی اتریبیوتی را می‌خوانید، پایتون اول Namespace مربوط به Instance را چک می‌کند؛ سپس به Namespace کلاس برمی‌گردد.
  • تخصیص به instance.attr همیشه متغیر Instanceای می‌سازد یا به‌روزرسانی می‌کند—even وقتی متغیر کلاسی با همان نام وجود دارد؛ هرگز متغیر کلاس را تغییر نمی‌دهد.
  • متغیر کلاسِ Mutable (یک List یا Dict) به اشتراک گذاشته می‌شود؛ پس Mutate کردنش از یک Instance بر هر Instanceای اثر می‌گذارد—این یکی از رایج‌ترین باگ‌های مبتدیان در پایتون است.
  • رفع باگ Mutable، تخصیص مقدار Mutable داخل __init__ است تا هر Instance نسخه خودش را بگیرد.
  • یک Subclass متغیرهای کلاس را از والدش به ارث می‌برد و می‌تواند با تخصیص نام در سطح Subclass آن‌ها را Override کند؛ که مقدار والد را از طریق ترتیب Resolution متد (MRO) Shadow می‌کند.
  • از vars(obj) یا obj.__dict__ برای بازرسی اتریبیوت‌های خودِ Instance و از ClassName.__dict__ برای بازرسی اتریبیوت‌های سطح-کلاس استفاده کنید.

پیش‌نیازها

باید پایتون ۳ نصب‌شده و محیط برنامه‌نویسی‌ای روی کامپیوتر یا سرورتان راه‌اندازی‌شده داشته باشید. اگر محیط برنامه‌نویسی ندارید، می‌توانید به راهنماهای نصب و راه‌اندازی برای محیط برنامه‌نویسی محلی یا محیط برنامه‌نویسی روی سرور مناسب سیستم‌عامل‌تان (اوبونتو، CentOS، دبیان و غیره) مراجعه کنید.

هم‌چنین داشتن درک پایه‌ای از کلاس‌ها و Objectهای پایتون کمک می‌کند؛ چون متغیرهای کلاس و Instance داخل تعاریف کلاس زندگی می‌کنند.

متغیرهای کلاس

متغیرهای کلاس درون تعریف کلاس تعریف می‌شوند. چون توسط خود کلاس مالکیت می‌شوند، متغیرهای کلاس توسط همه Instanceهای کلاس به اشتراک گذاشته می‌شوند. بنابراین عموماً برای هر Instance مقدار یکسانی خواهند داشت؛ مگر اینکه از متغیر کلاس برای مقداردهی اولیه متغیری استفاده کنید.

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

نکته: برای دنبال کردن کدهای نمونه در این tutorial، شل تعاملی پایتون را روی سیستم محلی‌تان با اجرای دستور python3 باز کنید. سپس می‌توانید مثال‌ها را بعد از پرامپت >>> کپی، پیست یا ویرایش کنید.

یک متغیر کلاس به‌تنهایی شبیه زیر است:

class Shark:
    animal_type = "fish"

اینجا، متغیر animal_type مقدار "fish" گرفته است. این یک Attribut است؛ که صرفاً مقدار نام‌داری است که به شیءای (در این مورد، کلاس) الصاق شده.

می‌توانیم Instanceای از کلاس Shark بسازیم (new_shark صدایش می‌کنیم) و متغیر را با استفاده از Dot Notation چاپ کنیم:

shark.py

class Shark:
    animal_type = "fish"

new_shark = Shark()
print(new_shark.animal_type)

بیایید برنامه را اجرا کنیم:

python shark.py

اجرای این برنامه خروجی زیر را برمی‌گرداند:

Output
fish

برنامه‌مان مقدار متغیر را برمی‌گرداند.

بیایید چند متغیر کلاس دیگر اضافه و چاپ‌شان کنیم:

shark.py

class Shark:
    animal_type = "fish"
    location = "ocean"
    followers = 5

new_shark = Shark()
print(new_shark.animal_type)
print(new_shark.location)
print(new_shark.followers)

مثل هر متغیر دیگری، متغیرهای کلاس می‌توانند از هر نوع داده‌ای موجود در پایتون تشکیل شوند. در این برنامه، رشته‌ها و یک عدد صحیح داریم. بیایید برنامه را دوباره با دستور python shark.py اجرا و خروجی را مرور کنیم:

Output
fish
ocean
5

Instance مربوط به new_shark قادر است به همه متغیرهای کلاس دسترسی پیدا و وقتی برنامه را اجرا می‌کنیم چاپ‌شان کند.

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

متغیرهای Instance

متغیرهای Instance توسط Instanceهای کلاس مالکیت می‌شوند. یعنی برای هر شیء یا Instance کلاس، متغیرهای Instance متفاوت‌اند.

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

در مثال کلاس Shark زیر، name و age متغیرهای Instance‌اند:

class Shark:
    def __init__(self, name, age):
        self.name = name
        self.age = age

کلیدواژه self به Instance مشخصِ در حال ساخت یا استفاده اشاره دارد. وقتی self.name = name را می‌نویسید، پایتون آن مقدار را روی شیء منفرد ذخیره می‌کند—نه روی کلاس؛ که این همان چیزی است که مقدار را برای هر Instance یکتا می‌سازد. وقتی شیء Shark می‌سازیم، باید این متغیرها را تعریف کنیم؛ که به‌عنوان پارامتر درون متد Constructor یا متد دیگری پاس می‌شوند:

class Shark:
    def __init__(self, name, age):
        self.name = name
        self.age = age

new_shark = Shark("Sammy", 5)

مثل متغیرهای کلاس، می‌توانیم به‌طور مشابه متغیرهای Instance را چاپ کنیم:

shark.py

class Shark:
    def __init__(self, name, age):
        self.name = name
        self.age = age

new_shark = Shark("Sammy", 5)
print(new_shark.name)
print(new_shark.age)

وقتی این برنامه را اجرا می‌کنیم، خروجی زیر را دریافت می‌کنیم:

Output
Sammy
5

خروجی‌ای که دریافت می‌کنیم از مقادیر متغیرهایی تشکیل شده که برای Instance شیءِ new_shark مقداردهی اولیه کردیم.

بیایید شیء دیگری از کلاس Shark به نام stevie بسازیم:

shark.py

class Shark:
    def __init__(self, name, age):
        self.name = name
        self.age = age

new_shark = Shark("Sammy", 5)
print(new_shark.name)
print(new_shark.age)

stevie = Shark("Stevie", 8)
print(stevie.name)
print(stevie.age)

خطوط هایلایت‌شده شیء دومی می‌سازند و مقادیرش را چاپ می‌کنند؛ که این خروجی را تولید می‌کند:

Output
Sammy
5
Stevie
8

شیء stevie—مثل شیء new_shark—پارامترهای خاصِ آن Instance از کلاس Shark را برای تخصیص مقادیر به متغیرهای Instance پاس می‌دهد.

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

پایتون چگونه Lookup متغیرها را Resolve می‌کند: Namespace مربوط به Instance در مقابل کلاس

وقتی اتریبیوتی را با Dot Notation می‌خوانید—مثل new_shark.animal_type—پایتون فقط در یک مکان نگاه نمی‌کند. از ترتیب Lookupی از طریق Namespaceها پیروی می‌کند. Namespace صرفاً نگاشتی از نام‌ها به مقادیر است؛ و هر Instance و هر کلاس Namespace خودش را دارد.

پایتون اول Namespace مربوط به Instance را چک می‌کند. اگر نام آنجا پیدا شد، آن مقدار برگردانده می‌شود. اگر پیدا نشد، پایتون سپس Namespace کلاس را چک می‌کند (و حین وراثت، کلاس‌های والد). Tutorial رسمی پایتون درباره کلاس‌ها همین تقدم را توصیف می‌کند: اتریبیوت‌های Instance بر اتریبیوت‌های کلاس با نام مشترک اولویت دارند.

این تقدم عاقبت مهمی دارد. تخصیص به instance.attribute همیشه مقداری در Namespace مربوط به Instance می‌سازد یا به‌روزرسانی می‌کند. هرگز متغیر کلاس را تغییر نمی‌دهد—even وقتی متغیر کلاسی با همان نام وجود دارد.

مثال زیر متغیر Instanceای را نشان می‌دهد که متغیر کلاسی را Shadow می‌کند:

shadow.py

class Shark:
    animal_type = "fish"

new_shark = Shark()
print(new_shark.animal_type)        # متغیر کلاس را می‌خواند

new_shark.animal_type = "shark"     # متغیر instance می‌سازد
print(new_shark.animal_type)        # متغیر instance جدید را می‌خواند
print(Shark.animal_type)            # متغیر کلاس بدون تغییر مانده

این خروجی را تولید می‌کند:

Output
fish
shark
fish

اولین print متغیر کلاس را می‌خواند؛ چون Instance هنوز animal_type خودش را ندارد. تخصیص، animal_type جدیدی در Namespace مربوط به Instance می‌سازد؛ پس دومین print آن مقدار Instance را می‌خواند. متغیر کلاس—که از طریق Shark.animal_type به آن دسترسی داریم—همچنان مقدار اصلی‌اش را نگه می‌دارد. این همان چیزی است که وقتی می‌گوییم «متغیر Instance متغیر کلاس را Shadow می‌کند» منظورمان است: مقدار کلاس را برای همان یک Instance مخفی می‌کند؛ بدون حذف یا تغییرش.

متغیرهای کلاس در مقابل متغیرهای Instance: مقایسه کنار هم

قبل از انتخاب اینکه از کدام نوع متغیر استفاده کنید، دیدن این دو در ابعادی که در کد واقعی بیشترین اهمیت را دارند کمک می‌کند. جدول زیر خلاصه می‌کند که متغیرهای کلاس و Instance چگونه در Scope، ذخیره‌سازی و رفتار تفاوت دارند:

بعدمتغیر کلاسمتغیر Instance
کجا تعریف می‌شوددر بدنه کلاس، خارج از هر متدداخل متد (معمولاً init) روی self
مالکیت توسطخود کلاسهر شیء منفرد
وضعیت مشترکتوسط همه Instanceها به اشتراکبرای هر Instance یکتا
دسترسی معمولClassName.var یا instance.varinstance.var
ذخیره درdict مربوط به کلاسdict مربوط به Instance
ریسک Mutabilityبالا: شیء Mutable مشترک بر هر Instance اثر می‌گذاردپایین: هر Instance شیء خودش را دارد
رفتار وراثتتوسط Subclassها از طریق MRO به ارثبه ارث نمی‌رسد؛ به ازای هر شیء در زمان اجرا ساخته می‌شود
استفاده رایجثابت‌ها، پیش‌فرض‌ها، شمارنده‌های مشترکداده به-ازای-شیء (مانند name یا age)

همان‌طور که جدول نشان می‌دهد، تفاوت مرکزی مالکیت است: متغیر کلاس در یک مکان زندگی می‌کند و توسط همه دیده می‌شود؛ در حالی که متغیر Instance برای هر شیء تازه ساخته می‌شود. همین تمایز واحد، ریسک Mutability بحث‌شده در بخش بعد را هدایت می‌کند.

دام متغیر کلاسِ Mutable

یکی از پرگزارش‌ترین اشتباهات مبتدیان در پایتون، استفاده از شیء Mutableای مانند List یا Dictionary به‌عنوان متغیر کلاس است. چون متغیر کلاس توسط هر Instance به اشتراک گذاشته می‌شود، Mutate کردنش از طریق یک شیء آن را برای همه‌شان تغییر می‌دهد.

مثال زیر منطقی به نظر می‌رسد اما حاوی این باگ است:

teeth.py

class Shark:
    teeth = []                    # متغیر کلاسِ mutable مشترک بین همه instanceها

    def __init__(self, name):
        self.name = name

sammy = Shark("Sammy")
stevie = Shark("Stevie")

sammy.teeth.append("incisor")     # به لیست مشترک append می‌کند
print(sammy.teeth)
print(stevie.teeth)

فراخوانی .append() لیست موجود را در-جای خودش Mutate می‌کند—نه اینکه اتریبیوت را دوباره تخصیص بدهد؛ پس پایتون هرگز متغیر Instance جدیدی نمی‌سازد. هر دو شیء به همان لیست مشترک اشاره می‌کنند؛ که این خروجی را تولید می‌کند:

Output
['incisor']
['incisor']

حتی though فقط به sammy.teeth append کردیم، تغییر روی stevie هم ظاهر می‌شود؛ چون هر دو نام به همان یک Listِ ذخیره‌شده روی کلاس اشاره می‌کنند. این به‌ندرت چیزی است که می‌خواهید.

رفع آن، تخصیص مقدار Mutable داخل __init__ است تا هر Instance شیء مستقل خودش را دریافت کند:

teeth.py

class Shark:

    def __init__(self, name):
        self.name = name
        self.teeth = []           # هر instance لیست خودش را می‌گیرد

sammy = Shark("Sammy")
stevie = Shark("Stevie")

sammy.teeth.append("incisor")
print(sammy.teeth)
print(stevie.teeth)

حالا هر شیء List جداگانه‌ای مالک می‌شود؛ پس append کردن به یکی دیگر بر دیگری اثر نمی‌گذارد؛ که خروجی تأییدش می‌کند:

Output
['incisor']
[]

قاعده سرانگشتی مفید این است: مقادیر Immutable مانند رشته‌ها، اعداد و Tupleها معمولاً به‌عنوان متغیرهای کلاس امن‌اند؛ در حالی که مقادیر Mutable مانند Listها، Dictionaryها و Setها اغلب به __init__ تعلق دارند وقتی هر Instance باید وضعیت خودش را نگه دارد.

کار با متغیرهای کلاس و Instance با هم

متغیرهای کلاس و Instance اغلب هم‌زمان استفاده می‌شوند؛ پس بیایید با استفاده از کلاس Sharkای که ساختیم به مثالی از این نگاه کنیم. کامنت‌های برنامه هر قدم فرایند را بیان می‌کنند:

shark.py

class Shark:

    # متغیرهای کلاس
    animal_type = "fish"
    location = "ocean"

    # متد constructor با متغیرهای instance یعنی name و age
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # متدی که پارامتر followers می‌پذیرد
    def set_followers(self, followers):
        print("This user has " + str(followers) + " followers")


def main():
    # اولین شیء، راه‌اندازی متغیرهای instance متد constructor
    sammy = Shark("Sammy", 5)

    # چاپ متغیر instance یعنی name
    print(sammy.name)

    # چاپ متغیر کلاس یعنی location
    print(sammy.location)

    # دومین شیء
    stevie = Shark("Stevie", 8)

    # چاپ متغیر instance یعنی name
    print(stevie.name)

    # صدا زدن set_followers و پاس مقدار followers
    stevie.set_followers(77)

    # چاپ متغیر کلاس یعنی animal_type
    print(stevie.animal_type)

if __name__ == "__main__":
    main()

وقتی این کد را اجرا می‌کنیم، خروجی زیر را دریافت می‌کنیم:

Output
Sammy
ocean
Stevie
This user has 77 followers
fish

اینجا، از هر دو متغیر کلاس و Instance در دو شیء از کلاس Shark یعنی sammy و stevie استفاده کرده‌ایم.

چه زمانی از متغیر کلاس به‌جای متغیر Instance استفاده کنیم

تصمیم‌گیری بین این دو به یک سوال خلاصه می‌شود: آیا مقدار به کلاس به‌عنوان یک کل تعلق دارد یا به یک شیء خاص؟ از متغیر کلاس وقتی استفاده کنید که مقدار واقعاً مشترک است—مانند مقدار پیکربندی ثابت، پیش‌فرضی که هر Instance با آن شروع می‌شود یا شمارنده‌ای که ردیابی می‌کند چند شیء ساخته شده. از متغیر Instance وقتی استفاده کنید که مقدار یک شیء خاص را توصیف می‌کند—مانند نام یا سنش.

شمارنده مثال روشنی از مقداری است که به کلاس—نه هیچ شیء منفردی—تعلق دارد:

counter.py

class Shark:
    total_sharks = 0              # متغیر کلاس مشترک بین همه instanceها

    def __init__(self, name):
        self.name = name
        Shark.total_sharks += 1   # به‌روزرسانی شمارنده مشترک با هر شیء جدید

sammy = Shark("Sammy")
stevie = Shark("Stevie")
print(Shark.total_sharks)

هر بار Constructor اجرا می‌شود، شمارنده مشترک روی کلاس را Increment می‌کند؛ پس شمارش هر شیءِ ساخته‌شده را منعکس می‌کند:

Output
2

دقت کنید شمارنده از طریق Shark.total_sharks—نه self.total_sharks—به‌روزرسانی می‌شود. نوشتن روی self.total_sharks متغیر Instanceای می‌ساخت و شمار مشترک را دست‌نخورده باقی می‌گذاشت؛ که دقیقاً همان رفتار Shadowing توصیف‌شده قبلی است.

متغیرهای کلاس و وراثت

متغیرهای کلاس در وراثت مشارکت می‌کنند؛ یعنی Subclass به‌طور خودکار به متغیرهای کلاسِ تعریف‌شده روی والدش دسترسی دارد. وقتی متغیر کلاسی را روی Subclass می‌خوانید، پایتون ترتیب Resolution متد (MRO)—فهرست مرتب کلاس‌هایی که پایتون جستجو می‌کند—را طی می‌کند؛ با شروع از Subclass و حرکت به سمت والدش.

در این مثال، Subclass مربوط به Hammerhead مقدار animal_type تعریف نکرده؛ پس خواندنش به کلاس والد Shark برمی‌گردد:

inherit.py

class Shark:
    animal_type = "fish"

class Hammerhead(Shark):
    pass

print(Hammerhead.animal_type)     # از Shark از طریق MRO به ارث رسیده

چون Hammerhead مقدار animal_type خودش را ندارد، پایتون آن را روی Shark پیدا می‌کند؛ که این خروجی را تولید می‌کند:

Output
fish

Subclass می‌تواند متغیر کلاسی را هم با تخصیص همان نام در سطح Subclass Override کند. این متغیر کلاس جداگانه‌ای روی Subclass می‌سازد که مقدار والد را بدون تغییر دادنش Shadow می‌کند:

inherit.py

class Shark:
    animal_type = "fish"

class Hammerhead(Shark):
    animal_type = "shark species"

print(Shark.animal_type)
print(Hammerhead.animal_type)

هر کلاس الان مقدار خودش را گزارش می‌کند؛ چون متغیر Subclass برای Hammerhead تقدم می‌یابد در حالی که Shark مقدار اصلی‌اش را نگه می‌دارد:

Output
fish
shark species

این بازتاب همان Shadowing سطح-Instance است که قبلاً دیدیم؛ یک سطح بالاتر در سلسله‌مراتب کلاس به‌جای یک شیء منفرد اعمال شده.

بازرسی متغیرها با dict و vars()

وقتی مطمئن نیستید اتریبیوتی روی Instance ذخیره شده یا روی کلاس، پایتون به شما اجازه می‌دهد هر دو Namespace را مستقیم بازرسی کنید. به‌طور پیش‌فرض، هر Instance از کلاس معمولی پایتون اتریبیوت __dict__ دارد؛ Dictionaryای که اتریبیوت‌های خودش را نگه می‌دارد؛ و تابع داخلی vars() همان Dictionary را برمی‌گرداند.

مثال زیر اتریبیوت‌های خودِ یک Instance را بازرسی می‌کند:

inspect.py

class Shark:
    animal_type = "fish"          # متغیر کلاس

    def __init__(self, name, age):
        self.name = name          # متغیرهای instance
        self.age = age

sammy = Shark("Sammy", 5)
print(sammy.__dict__)
print(vars(sammy))

هر دو فراخوانی همان Dictionary را برمی‌گردانند؛ که فقط شامل متغیرهای Instance است و نه متغیر کلاسِ animal_type:

Output
{'name': 'Sammy', 'age': 5}
{'name': 'Sammy', 'age': 5}

برای دیدن به‌جایش اتریبیوت‌های سطح-کلاس، __dict__ مربوط به کلاس را بازرسی کنید:

inspect.py

class Shark:
    animal_type = "fish"

    def __init__(self, name, age):
        self.name = name
        self.age = age

print(Shark.__dict__["animal_type"])

خواندن کلید animal_type از Dictionary کلاس، مقدار متغیر کلاس را برمی‌گرداند:

Output
fish

بازرسی __dict__ راه قابل اطمینانی برای تأیید اینکه مقدار واقعاً کجا زندگی می‌کند است؛ که به‌ویژه هنگام دیباگ رفتار Shadowing و متغیر Mutableِ توصیف‌شده در بخش‌های قبلی مفید است.

مقایسه متغیر کلاس پایتون با فیلد Static در Java یا C++

توسعه‌دهندگانی که از زبان‌های Type-شده ایستا مانند Java یا C++ می‌آیند، اغلب ایده فیلد Static را روی متغیر کلاس پایتون نگاشت می‌کنند؛ و این مقایسه نقطه شروع مفیدی است؛ چون هر دو به کلاس—نه به شیء منفرد—تعلق دارند. اما متغیرهای کلاس پایتون چند روش مهم از فیلدهای Static انعطاف‌پذیرترند.

فیلد Static در Java یا C++ با کلیدواژه static و نوع ثابتی تعریف می‌شود و نمی‌تواند به-ازای-شیء Shadow شود. فیلدهای Static با خود کلاس در ارتباط می‌مانند. در پایتون کلیدواژه static و تعریف نوع وجود ندارد؛ هر Instanceای می‌تواند با تخصیص به همان نام، متغیر کلاسی را Shadow کند؛ و متغیرهای کلاس حین وراثت در ترتیب Resolution متد مشارکت می‌کنند—نه اینکه صرفاً در زمان کامپایل Resolve شوند. نتیجه عملی این است: متغیر کلاس پایتون مانند پیش‌فرض مشترکی رفتار می‌کند که هر Instance یا Subclassای می‌تواند بی‌سروصدا Override‌اش کند؛ پس نباید فرض کنید همان مقدار همه‌جا دیده می‌شود—مثل کاری که با فیلد Static واقعی ممکن بود بکنید.

سوالات متداول

۱. تفاوت متغیر کلاس و متغیر Instance در پایتون چیست؟

متغیر کلاس در سطح کلاس تعریف شده و توسط همه Instanceهای آن کلاس به اشتراک گذاشته می‌شود؛ در حالی که متغیر Instance داخل __init__ (یا متد دیگری) با استفاده از self تعریف شده و برای هر شیء منفرد یکتاست. متغیرهای کلاس برای مقادیری خوب‌اند که به کلاس به‌عنوان کل تعلق دارند—مانند ثابت‌ها یا شمارنده‌های مشترک؛ در حالی که متغیرهای Instance داده به-ازای-شیء مانند name یا age را نگه می‌دارند. این دو اغلب با هم در همان کلاس استفاده می‌شوند.

۲. آیا متغیر Instance می‌تواند متغیر کلاس را Override کند؟

بله. اگر مقداری را به اتریبیوتی روی Instanceای که نامی مشترک با متغیر کلاس دارد تخصیص بدهید، پایتون ورودی جدیدی در __dict__ همان Instance می‌سازد؛ که متغیر کلاس را فقط برای همان Instance Shadow می‌کند. خود متغیر کلاس تغییر نمی‌کند؛ پس Instanceهای دیگر همچنان مقدار کلاس اصلی را می‌بینند. می‌توانید با خواندن ClassName.attribute تأییدش کنید؛ که مقدار کلاسِ بدون تغییر را ادامه می‌دهد به برگرداندن.

۳. چه زمانی باید به‌جای متغیر Instance از متغیر کلاس استفاده کنم؟

از متغیر کلاس وقتی استفاده کنید که مقداری واقعاً بین همه Instanceها مشترک است و به خود کلاس تعلق دارد—مانند شمارنده‌ای از اینکه چند شیء ساخته شده، مقدار پیکربندی ثابت، یا پیش‌فرضی که همه Instanceها به آن ارجاع می‌دهند مگر اینکه به‌صورت منفرد Override شود. از متغیر Instance برای هر مقداری که باید از شیئی به شیء بعدی متفاوت باشد استفاده کنید. اگر یافتید خودتان را در حال سؤال هستید که آیا مقداری مشترک است، معمولاً به Instance تعلق دارد.

۴. چرا استفاده از شیء Mutable (مثل List یا Dict) به‌عنوان متغیر کلاس خطرناک است؟

چون همه Instanceها همان شیء را به اشتراک می‌گذارند؛ Mutate کردنش از یک Instance—مثلاً append کردن به List—بر هر Instance دیگری اثر می‌گذارد. این یکی از رایج‌ترین منابع باگ‌های غیرمنتظره در کد شیءگرای پایتون است. رفعش، تخصیص پیش‌فرض Mutable داخل __init__ است تا هر Instance نسخه مستقل خودش را بگیرد؛ در حالی که متغیرهای کلاس را برای مقادیر Immutable مانند رشته‌ها، اعداد و Tupleها نگه می‌دارید.

۵. self هنگام تنظیم متغیرهای Instance چه می‌کند؟

self به Instance مشخصِ در حال ساخت یا استفاده اشاره دارد. وقتی self.name = value را داخل __init__ می‌نویسید، پایتون آن جفت کلید-مقدار را در __dict__ خودِ Instance ذخیره می‌کند؛ که آن را برای همان شیء یکتا می‌سازد. بدون self، تخصیص متغیر محلی‌ای می‌ساخت که وقتی متد return می‌کند ناپدید می‌شود—نه اینکه روی شیء باقی بماند.

۶. متغیرهای کلاس در وراثت چگونه رفتار می‌کنند؟

Subclass متغیرهای کلاس را از والدش به ارث می‌برد. اگر Subclass مقدار جدیدی را در سطح کلاس به آن متغیر تخصیص بدهد، متغیر کلاس خودش را می‌سازد که مقدار والد را فقط برای همان Subclass Shadow می‌کند. اگر Subclass فقط متغیر را بخواند، پایتون از طریق ترتیب Resolution متد (MRO)—زنجیره مرتب کلاس‌هایی که پایتون حین Lookup جستجو می‌کند—به متغیر کلاس والد برمی‌گردد.

۷. چطور می‌توانم بازرسی کنم که شیء چه متغیرهای Instance و کلاسی دارد؟

از vars(instance) یا instance.__dict__ برای دیدن اتریبیوت‌های سطح-Instance؛ و از ClassName.__dict__ یا vars(ClassName) برای دیدن اتریبیوت‌های سطح-کلاس استفاده کنید. دقت کنید instance.__dict__ متغیرهای کلاس را شامل نمی‌شود—مگر اینکه در سطح Instance Shadow شده باشند؛ که این آن را به راه سریعی برای چک کردن اینکه مقدار خاصی واقعاً کجا ذخیره شده تبدیل می‌کند.

۸. آیا متغیر کلاس پایتون همان متغیر Static در Java یا C++ است؟

از نظر مفهومی مشابه‌اند؛ چون هر دو به کلاس—نه به شیء منفرد—تعلق دارند. اما متغیرهای کلاس پایتون انعطاف‌پذیرترند: می‌توانند به-ازای-Instance با Shadow کردن Override شوند، حین وراثت در MRO مشارکت می‌کنند و کلیدواژه static یا تعریف نوع صریح لازم ندارند. متغیر کلاس پایتون را به‌عنوان پیش‌فرض مشترکی رفتار کنید که هر Instance یا Subclassای می‌تواند Override کند؛ نه ثابت زمان-کامپایل ثابتی.

نتیجه‌گیری

در برنامه‌نویسی شیءگرا، متغیرها در سطح کلاس به‌عنوان متغیرهای کلاس ارجاع می‌شوند؛ در حالی که متغیرها در سطح شیء متغیرهای Instance نامیده می‌شوند.

متغیرهای کلاس برای مقادیر مشترک بین همه Instanceها مفیدند؛ در حالی که متغیرهای Instance داده یکتای هر شیء را ذخیره می‌کنند. درک ترتیب Lookup، دام متغیر کلاسِ Mutable و نحوه رفتار این متغیرها تحت وراثت، کمک می‌کند از برخی از رایج‌ترین باگ‌ها در کد شیءگرای پایتون اجتناب کنید.

استفاده از متغیرهای خاص-کلاس و خاص-Instance می‌تواند تضمین کند کدمان به اصل DRY پایبند باشد تا تکرار درون کد کم شود.

نوشته های مشابه

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

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

دکمه بازگشت به بالا