پاسخ تمرین‌های فصل دوم: مفاهیم بنیادین — کلاس، شیء، self و سازنده

Please login to bookmark Close

پاسخ‌ها به‌ترتیب پرسش‌های 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 و محل زندگی داده، از این دسته باگ‌های خاموش جلوگیری می‌کند.

Please login to bookmark Close
نظرات

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

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم