پاسخها بهترتیب پرسشهای
02-fundamentals-class-object-self-constructor-exercise.md. جایی که راه دیگری هم بود، بخش راه دیگر اضافه شده است.
تمرین ۱ – پرسشهای مفهومی
۱. کلاس مثل نقشهی یک خانه است: خودش خانه نیست، فقط توضیح میدهد خانههایی از این نوع چه شکلیاند. شیء یک خانهی واقعی ساختهشده از روی آن نقشه است. از روی یک نقشه میتوان صدها خانه ساخت؛ از روی یک کلاس هزاران شیء.
type(u1)→<class '__main__.User'>— ثابت میکندu1نمونهای از کلاسUserاست.u1 is u2→False— ثابت میکند اینها دو موجود کاملاً مستقل در حافظهاند؛ تغییر یکی اثری بر دیگری ندارد.
۲. self ارجاعی است به همان شیئی که متد روی آن فراخوانی شده. یک کلمهی کلیدی جادویی نیست؛ فقط یک قرارداد نامگذاری برای اولین پارامتر هر متد است (میتوانستید اسمش را چیز دیگری بگذارید، اما نگذارید). وقتی u.greet() را صدا میزنید، پایتون پشت پرده u را بهعنوان اولین پارامتر پاس میدهد؛ یعنی u.greet() دقیقاً معادل User.greet(u) است. برای همین self همیشه اولین پارامتر است: چون همان شیء صاحب فراخوانی است.
۳. در پایتون متغیرها «برچسب»اند نه «جعبه». وقتی u = User("ali") مینویسید، شیء در بخشی از حافظه به نام Heap ساخته میشود و u فقط یک ارجاع (اشارهگر) به آن است. self هم دقیقاً به همان شیء روی Heap اشاره میکند — نه به یک کپی. پس وقتی self.username = "reza" را اجرا میکنید، همان شیء واقعی تغییر میکند و تغییر ماندگار است. اگر self یک کپی بود، تغییرات بعد از پایان متد از بین میرفتند.
۴.
- Instance Variable: به هر شیء جداگانه تعلق دارد، معمولاً درون
__init__باself.x = ...تعریف میشود. هر شیء نسخهی خودش را دارد. برای دادهی مختص هر شیء (نام، موجودی) از این استفاده کنید. - Class Variable: به خود کلاس تعلق دارد و بین همهی اشیاء مشترک است، مستقیماً در بدنهی کلاس (بیرون متدها) تعریف میشود. برای دادهای که واقعاً باید مشترک باشد (نام شرکت، یک شمارندهی سراسری، تنظیمات پیشفرض) از این استفاده کنید.
۵.
__new__شیء خام را میسازد و برمیگرداند (پارامترclsمیگیرد).__init__روی آن شیء اجرا میشود و آن را مقداردهی میکند (پارامترselfمیگیرد، چیزی برنمیگرداند).
یعنی __init__ شیء را نمیسازد؛ شیء پیش از آن ساخته شده و __init__ فقط پُرش میکند. در ۹۹٪ موارد فقط __init__ را مینویسید. __new__ فقط در موارد نادر لازم میشود: الگوی Singleton، ارثبری از انواع تغییرناپذیر مثل int و tuple (که مقدارشان باید در زمان ساخت قطعی شود)، و بعضی کارهای متاکلاسی.
تمرین ۲ – دامها و کدخوانی
۶. خروجی:
1
0
self.count += 1 در واقع self.count = self.count + 1 است. سمت راست (self.count) هنوز instance variableای وجود ندارد، پس مقدار را از Class Variable میخواند (۰). اما سمت چپ یک instance variable جدید روی self میسازد و مقدار ۱ را در آن میگذارد. از این لحظه، شیء a یک count مختص خودش دارد که Class Variable را «سایه» میزند. به همین دلیل a.count == 1 ولی Counter.count مشترک دستنخورده و برابر ۰ میماند.
۷. خروجی: ['ali']. این یک دام است چون members = [] یک Class Variable تغییرپذیر است که بین همهی نمونهها مشترک است. t1.add("ali") روی همان لیست مشترک append میکند، پس t2 هم همان را میبیند — یک «نشتی» ناخواسته. اصلاح:
class Team:
def __init__(self):
self.members = [] # each team has its own independent list
def add(self, name):
self.members.append(name)
حالا هر بار که شیء ساخته میشود، یک لیست تازه ساخته میشود و نشتی از بین میرود.
۸. متد تابعی است که درون یک کلاس تعریف شده و به شیء گره خورده؛ بهطور خودکار self (شیء صاحب) را دریافت میکند و به حالت شیء دسترسی دارد. تابع معمولی مستقل است و هر دادهای که لازم دارد باید صریحاً بهعنوان پارامتر به آن پاس داده شود. متد به self نیاز دارد چون باید بداند روی کدام شیء کار میکند؛ تابع مستقل چنین شیئی ندارد.
تمرین ۳ – کدنویسی
۹.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
r = Rectangle(3, 4)
print(r.area()) # 12
print(r.perimeter()) # 14
۱۰. شمردن تعداد نمونهها با Class Variable:
class Widget:
count = 0 # shared Class Variable
def __init__(self, name):
self.name = name
Widget.count += 1 # increment on the class itself
Widget("a")
Widget("b")
Widget("c")
print(Widget.count) # 3
نکتهی مهم: حتماً Widget.count += 1 بنویسید، نه self.count += 1. اگر self.count بنویسید، به همان دام سوال ۶ میافتید و بهجای افزایش شمارندهی مشترک، یک instance variable میسازید.
راه دیگر (با type(self) برای سازگاری با ارثبری): اگر بخواهید زیرکلاسها شمارندهی جدا داشته باشند یا کد نسبت به نام کلاس مقاوم باشد، میتوانید از type(self) استفاده کنید — اما بدانید در ارثبری رفتار ظریف میشود. برای شروع همان Widget.count += 1 واضحترین است.
۱۱. BankAccount با اعتبارسنجی و شناسهی یکتا:
class BankAccount:
_next_id = 1000 # shared counter for the ID
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.account_id = BankAccount._next_id
BankAccount._next_id += 1
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit amount must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("insufficient balance")
self.balance -= amount
return self.balance
a1 = BankAccount("ali", 1000)
a2 = BankAccount("sara")
print(a1.account_id, a2.account_id) # 1000 1001
a1.deposit(500)
print(a1.balance) # 1500
۱۲. بله، کار میکند. خروجی:
ali@example.com
False
پایتون پویاست؛ میتوانید بعد از ساخت شیء یک attribute جدید (email) به آن اضافه کنید. اما این attribute فقط روی همان شیء u مینشیند، نه روی کلاس و نه روی نمونههای دیگر. برای همین hasattr(other, "email") برابر False است. (نکتهی سبک: هرچند این کار ممکن است، معمولاً بهتر است همهی attributeها را در __init__ تعریف کنید تا کد قابلپیشبینی بماند.)
تمرین ۴ – مقایسه و تحلیل
۱۳.
| ویژگی | باحالت (Stateful) | بیحالت (Stateless) |
|---|---|---|
| نگهداشتن دادهی داخلی | دارد؛ حالت در طول زمان تغییر میکند | ندارد؛ حالت داخلی متغیر نگه نمیدارد |
| وابستگی خروجی | به تاریخچهی فراخوانیها بستگی دارد | فقط به ورودیها بستگی دارد (همان ورودی → همان خروجی) |
| یک مثال واقعی | حساب بانکی، سبد خرید، Accumulator | اعتبارسنج، PriceFormatter |
۱۴. چون منطق «مالکیت داده» فرق میکند:
ownerوbalanceمختص هر حساباند — حساب علی موجودی خودش را دارد و حساب سارا موجودی خودش را. پس Instance Variable.bank_nameو_next_idواقعاً مشترکاند — همهی حسابها در یک بانکاند (پس نام بانک یکی است)، و شمارندهی شناسه باید سراسری و یکتا باشد تا شناسهها تکراری نشوند. پس Class Variable.
قاعدهی تصمیم: از خودتان بپرسید «این داده باید مختص هر شیء باشد یا مشترک بین همه؟».
تمرین ۵ – پرسش تحلیلی
۱۵. این جمله مطلقگرایانه است و با مفهوم Tradeoff نمیخواند. درست است که اشیای بیحالت مزیت دارند (امنیت در برنامههای چندنخی، سادگی تست، رفتار قابلپیشبینی)، اما همهچیز را نمیتوان بیحالت کرد. بسیاری از موجودیتهای دنیای واقعی ذاتاً حالت دارند: حساب بانکی باید موجودی را نگه دارد، سبد خرید باید آیتمها را بهخاطر بسپارد، یک بازیکن باید جان و امتیازش را حفظ کند. اگر بخواهیم اینها را بهزور بیحالت کنیم، مجبور میشویم حالت را بهشکل زشتی از بیرون پاس بدهیم و پیچیدگی را جای دیگری منتقل کنیم. پاسخ درست «همیشه X» نیست، بلکه «بسته به ماهیت مسئله» است: جایی که منطق واقعاً بیحالت است بیحالت طراحی کنید، و جایی که موجودیت ذاتاً حالت دارد، حالت را صادقانه در همان شیء نگه دارید.
۱۶. این دو سوءتفاهم پرخطرند چون بیسروصدا به باگ منجر میشوند — کد اجرا میشود، اما نتیجه اشتباه است. مثال: تازهکاری که فرق Class Variable و Instance Variable را نمیداند، ممکن است بنویسد:
class ShoppingCart:
items = [] # wrong: shared among all carts!
def add(self, p):
self.items.append(p)
حالا سبد خرید هر کاربر، آیتمهای کاربران دیگر را هم نشان میدهد — یک فاجعهی امنیتی/منطقی که هیچ خطایی هم نمیدهد. ریشه دقیقاً همان است: نفهمیدن اینکه داده کجا زندگی میکند (روی کلاس یا روی شیء). درک درست self و محل زندگی داده، از این دسته باگهای خاموش جلوگیری میکند.