پاسخ تمرین‌های فصل چهارم: اصول چهارگانه شیءگرایی

Please login to bookmark Close

پاسخ‌ها به‌ترتیب پرسش‌های 04-four-pillars-exercise.md. هرجا راه دیگری هم بود، بخش راه دیگر آورده شده تا خلاقیت شما در طراحی بیشتر شود.

تمرین ۱ – ارث‌بری

۱. ارث‌بری مشکل کد تکراری را حل می‌کند: رفتار مشترک یک‌بار در کلاس والد نوشته می‌شود و همه‌ی فرزندان آن را به ارث می‌برند؛ در نتیجه هر تغییر آینده هم فقط یک‌جا اعمال می‌شود. آزمون Is-A یعنی پیش از ارث‌بری، جمله‌ی «X یک نوع Y است» را بلند بگویید؛ اگر جمله طبیعی نبود، ارث‌بری غلط است:

  • Manager(Employee) — درست؛ «مدیر یک نوع کارمند است.»
  • Car(Engine) — غلط؛ خودرو موتور دارد (Has-A). باید یک Engine درون Car نگه داشته شود (ترکیب).
  • Penguin(Bird) — درست؛ «پنگوئن یک نوع پرنده است.» (و نمونه‌ی خوبی از ارث‌بری چندسطحی: پنگوئن ← پرنده ← حیوان.)
  • Order(OrderItem) — غلط؛ سفارش شامل قلم‌هاست، نه نوعی از قلم. رابطه Has-A است و جایش یک لیست از OrderItem درون Order است.

۲. super() دسترسی به پیاده‌سازی والد می‌دهد («همین متد را از والد اجرا کن»). دو دلیل برای ترجیحش بر نام مستقیم والد: اول اینکه نام والد را در بدنه‌ی فرزند حک نمی‌کند و با تغییر والد، کد فرزند سالم می‌ماند؛ دوم اینکه در وراثت چندگانه مسیر درست را طبق MRO دنبال می‌کند و از اجرای دوباره‌ی یک والد جلوگیری می‌کند (فصل ششم).

کد داده‌شده هنگام print(b.name) با AttributeError می‌شکند: سازنده‌ی BrokenManager هرگز super().__init__(name, salary) را صدا نزده، پس self.name و self.salary اصلاً ساخته نشده‌اند. خود ساختن شیء خطا نمی‌دهد — خطا در اولین استفاده از attribute گم‌شده ظاهر می‌شود، و همین تأخیر است که چنین باگ‌هایی را موذی می‌کند.

الگوی *args/**kwargs برای این است که فرزند مجبور نباشد پارامترهای والد را تکرار کند: فقط پارامترهای خودش را نام می‌برد (این‌جا team_size، که بعد از *args فقط‌کلیدواژه‌ای هم شده) و بقیه را دست‌نخورده به والد پاس می‌دهد — «امضای والد را فقط والد بداند». مزیت: اگر امضای والد تغییر کند (پارامتر جدید یا تغییر نام)، هیچ زیرکلاسی نیاز به ویرایش ندارد؛ و در وراثت چندگانه که super() ممکن است کلاسی ناشناخته را صدا بزند، این الگو عملاً اجباری است (فصل ششم). هزینه: امضای فرزند دیگر خودگویا نیست — help() و ویرایشگر نمی‌گویند این کلاس در نهایت چه ورودی‌هایی می‌پذیرد، و خطای نام اشتباه آرگومان، یک پله دورتر (در والد) ظاهر می‌شود.

۳.

  • Override کامل: والد را به‌کل کنار می‌گذاریم و متد را از نو می‌نویسیم.
  • Override توسعه‌ای: رفتار والد را با super() می‌گیریم و چیزی به آن اضافه می‌کنیم.
class Employee:
    def describe(self):
        return "an employee"

class Contractor(Employee):
    def describe(self):                    # full override
        return "a contractor"

class Manager(Employee):
    def describe(self):                    # extending override
        return super().describe() + ", also managing a team"

print(Contractor().describe())   # a contractor
print(Manager().describe())      # an employee, also managing a team

قاعده‌ی انتخاب: اگر منطق والد هنوز معتبر است و فقط مکمل می‌خواهد، توسعه‌ای (تا منطق والد را کپی نکنید)؛ اگر رفتار فرزند از اساس متفاوت است، کامل.

۴.

  • ساده (Single): یک فرزند از یک والد. مثال: «حساب پس‌انداز» از «حساب بانکی».
  • چندسطحی (Multilevel): زنجیره‌ی نسل‌ها. مثال: پنگوئن ← پرنده ← حیوان.
  • سلسله‌مراتبی (Hierarchical): چند فرزند از یک والد مشترک. مثال: مدیر، توسعه‌دهنده و طراح، هر سه از «کارمند».
  • چندگانه (Multiple): یک فرزند از چند والد. مثال: گوشی هوشمند هم «دوربین» است و هم «تلفن».

پرکاربردترین در پروژه‌های واقعی، سلسله‌مراتبی است (یک پایه‌ی مشترک، چند تخصص). ریسک اصلی وراثت چندگانه، ابهام در متدهای هم‌نام والدهاست («کدام برنده می‌شود؟») که پاسخش به MRO و مسئله‌ی الماس برمی‌گردد — موضوع فصل ششم.

۵. سه دلیل اصلی: خطاها معنادار و مخصوص دامنه‌ی پروژه می‌شوند (InsufficientFundsError به‌جای ValueError عمومی)؛ گیرنده می‌تواند انتخابی خطا بگیرد و خطاهای بی‌ربط را نبلعد؛ و نام استثنا خودش مستندات است. مزیت سلسله‌مراتب: با یک except PaymentError همه‌ی خطاهای پرداخت (هر تعداد زیرکلاس) یک‌جا گرفته می‌شوند، و هر جا لازم شد، یک زیرکلاس خاص را می‌توان جداگانه مدیریت کرد. سلسله‌مراتب استثناها همان سلسله‌مراتب ارث‌بری است (تکمیل در فصل یازدهم).

تمرین ۲ – انتزاع

۶. انتزاع یعنی نمایش‌دادن فقط جزئیات ضروری به استفاده‌کننده و پنهان‌کردن سازوکار پشت آن. مثال غیربرنامه‌نویسی: فرمان خودرو — «جهت را عوض کن» را در اختیارتان می‌گذارد و جعبه‌فرمان و هیدرولیک را پنهان می‌کند. دو مثال پایتونی که هر روز مصرف کرده‌اید: list.sort() (الگوریتم Timsort پشت آن پنهان است) و open() (تعامل با سیستم‌عامل و دیسک پنهان است). در هر دو، «نیاز نداشتن به دانستن» همان چیزی است که کارکردن را ممکن می‌کند.

۷. نرم‌افزار از لایه‌های انتزاع ساخته می‌شود که هر لایه، پیچیدگی لایه‌ی زیرین را پنهان می‌کند. اهمیتش این است که در هر لحظه فقط باید در یک سطح فکر کنید و بار ذهنی محدود می‌ماند. در آن زنجیره: notify_user منطق «به کاربر خبر بده» را دارد و نمی‌داند پیامک چطور ارسال می‌شود؛ sms.send جزئیات ساخت و ارسال پیامک را دارد و نمی‌داند HTTP چطور کار می‌کند؛ requests.post هم سوکت و TCP را از همه پنهان کرده است. هر لایه فقط زبان لایه‌ی زیرین خودش را بلد است، نه عمق آن را.

۸. ABC انتزاع را از «توصیه» به قرارداد الزام‌آور زبانی تبدیل می‌کند. Shape() قابل ساختن نیست چون کلاسی با متد انتزاعیِ پیاده‌نشده، مفهومی ناقص است و پایتون ساخت نمونه از آن را با TypeError رد می‌کند. اگر زیرکلاسی متد انتزاعی را پیاده نکند، خودش هم انتزاعی می‌ماند و در لحظه‌ی ساختن شیء (نه لحظه‌ی صدازدن متد) TypeError می‌گیرد. این لحظه مهم است چون خطا زود و نزدیک به علت ظاهر می‌شود — به‌جای آنکه سه هفته بعد، وسط اجرای عملیات در محیط واقعی، تازه معلوم شود متدی جا افتاده (اتفاقی که در Duck Typing ممکن است).

۹. یعنی متدهای عمومی کلاس باید زبان مسئله‌ی استفاده‌کننده را حرف بزنند، نه زبان پیاده‌سازی ما را. send(to, message) دقیقاً همان چیزی است که استفاده‌کننده می‌خواهد بگوید: «این متن را به این شماره بفرست» — او هیچ‌چیز درباره‌ی سازوکار HTTP یاد نمی‌گیرد و نباید یاد بگیرد. اما رابط set_auth_header(...) + post_json(...) ساختار درونی ما را به بیرون نشت می‌دهد: استفاده‌کننده باید بداند اول احراز هویت است بعد ارسال، فرمت بدنه چیست و اگر ترتیب را اشتباه برود، خطا می‌گیرد. با رابط اول، تعویض کامل پیاده‌سازی (حتی تغییر شرکت پیامکی) کد هیچ استفاده‌کننده‌ای را نمی‌شکند؛ با رابط دوم، هر تغییر درونی یک تغییر سراسری است.

تمرین ۳ – چندریختی

۱۰. چندریختی یعنی توانایی کار با اشیای مختلف از طریق یک پیام مشترک، بی‌آنکه نوع دقیقشان را بدانیم: یک فراخوانی، رفتارهای متفاوت. تفاوت با Override در نسبت ابزار و نتیجه است: Override مکانیزم است (زیرکلاس متد والد را بازنویسی می‌کند)؛ چندریختی نتیجه است (می‌توان با همه‌ی انواع از یک دریچه کار کرد). Override یکی از راه‌های رسیدن به چندریختی است — Duck Typing نشان می‌دهد که تنها راهش نیست.

۱۱. Duck Typing یعنی هویت شیء را از رفتارش نتیجه بگیریم، نه از تبارش: «اگر مثل اردک راه می‌رود و صدا می‌کند، اردک است.» تابع total_area فقط یک چیز از عناصر می‌خواهد: داشتن متد area(). برایش مهم نیست شیء از چه کلاسی آمده یا والد مشترکی در کار هست یا نه؛ پیام را می‌فرستد و هر شیء به شیوه‌ی خودش پاسخ می‌دهد. بهایش: هیچ بررسی زودهنگامی وجود ندارد — اگر عنصری area نداشته باشد یا آن را اشتباه پیاده کرده باشد، خطا اولین‌بار در زمان اجرا ظاهر می‌شود، نه در لحظه‌ی تعریف کلاس.

۱۲. سه مسیر: ارث‌بری + Override (والد مشترک، بازنویسی در فرزندان)، Duck Typing (بدون خویشاوندی؛ فقط رفتار مشترک)، و قرارداد رسمی با ABC (رابط الزام‌آور + پیاده‌سازی در زیرکلاس‌ها).

  • اسکریپت شخصی کوچک: Duck Typing — انعطاف و کم‌تشریفاتی مهم‌تر از ضمانت است؛ ABC این‌جا بوروکراسی اضافه است.
  • درگاه‌های پرداخت در تیم بیست‌نفره: ABC — مرز حساس سیستم است و باید ضمانت زبانی وجود داشته باشد که هیچ درگاهی متدی را جا نینداخته؛ خطای زودهنگام به‌صرفه‌تر از باگ در محیط عملیاتی است.
  • کارمندان با رفتار مشترک قابل‌توجه: ارث‌بری + Override — چون علاوه بر رابط مشترک، پیاده‌سازی مشترک هم هست که باید یک‌جا نوشته شود؛ والد واقعی (نه صرفاً انتزاعی) دقیقاً همین را می‌دهد.

۱۳. Overriding: زیرکلاس متد والد را با همان نام بازنویسی می‌کند (بین دو کلاس). Overloading: چند نسخه از یک متد با امضاهای متفاوت در همان کلاس، که کامپایلر بر اساس آرگومان‌ها انتخاب می‌کند (مثل جاوا). پایتون Overloading به این معنا ندارد — تعریف هم‌نام دوم، اولی را جایگزین می‌کند. جایگزین پایتونی: مقدار پیش‌فرض و آرگومان‌های کلیدواژه‌ای:

class Report:
    def export(self, path, fmt="pdf", compress=False):
        return f"{path} -> {fmt} (compress={compress})"

r = Report()
print(r.export("a"))                    # a -> pdf (compress=False)
print(r.export("a", fmt="csv"))         # a -> csv (compress=False)
print(r.export("a", compress=True))     # a -> pdf (compress=True)

یک متد، چندین شکل فراخوانی — بدون هیچ نسخه‌ی دومی.

تمرین ۴ – کپسول‌سازی

۱۴. کپسول‌سازی یعنی بسته‌بندی داده و رفتار مربوط در یک واحد و کنترل دسترسی به آن داده. در کد مثال، acc.balance = -5000 بدون هیچ مانعی موجودی را منفی می‌کند و ناوردای کلاس — قانونی که باید همیشه برقرار بماند: «موجودی هرگز منفی نمی‌شود» — نقض می‌شود. کپسول‌سازی همه‌ی مسیرهای تغییر داده را پشت گیت‌های بازرسی جمع می‌کند: داده _balance می‌شود و تنها راه کم‌کردنش متد withdraw است که پیش از تغییر، قانون را چک می‌کند و در صورت تخلف ValueError می‌دهد. ناوردا وقتی تضمین می‌شود که هیچ مسیر دورزدنی باقی نمانده باشد.

۱۵.

نام‌گذارینام سطحمعنا
namePublicآزادانه قابل استفاده در همه‌جا
_nameProtected«داخلی است؛ لطفاً از بیرون دست نزن» (فقط قرارداد، بدون مانع فنی)
__namePrivateName Mangling فعال می‌شود؛ مخصوص خود کلاس

۱۶. Name Mangling یعنی پایتون نام‌های __x درون کلاس را در پشت‌صحنه به _ClassName__x تغییر می‌دهد: __pin در Account به _Account__pin تبدیل می‌شود. مکانیزم امنیتی واقعی نیست — a._Account__pin هنوز کار می‌کند؛ فقط دسترسی را سخت و زشت می‌کند.

هدف اصلی، جلوگیری از تصادم نام در ارث‌بری است. در سناریوی Logger/TimedLogger: والد شمارنده‌ای به نام __count دارد که به _Logger__count تبدیل می‌شود، و فرزند هم __count خودش را تعریف می‌کند که به _TimedLogger__count تبدیل می‌شود — دو attribute کاملاً جدا. نویسنده‌ی فرزند حتی لازم نیست بداند والد درون خودش چه نام‌هایی دارد. بدون Name Mangling، مقداردهی فرزند شمارنده‌ی والد را بازنویسی می‌کرد و متد log والد بی‌سروصدا خراب می‌شد — باگی که پیداکردنش دشوار است، چون هیچ خطایی رخ نمی‌دهد؛ فقط اعداد غلط می‌شوند.

۱۷. یعنی «این‌جا همه‌ی ما بزرگسالیم»: پایتون به‌جای قفل و اجبار، به مسئولیت‌پذیری برنامه‌نویس تکیه می‌کند — زیرخط‌ها علامت‌اند، نه دیوار. Tradeoff: جاوا با private اجباری، خطای کمتر اما انعطاف کمتر می‌دهد (گاهی برای تست یا دیباگ اضطراری به دسترسی نیاز دارید و راهی نیست)؛ پایتون آزادی و انعطاف می‌دهد اما مسئولیت را به شما می‌سپارد و اگر آگاهانه عبور کنید، ردپای زشتش (_Account__pin) در Code Review دیده می‌شود. هیچ‌کدام مطلقاً بهتر نیستند؛ دو پاسخ متفاوت به سوال «به برنامه‌نویس چقدر اعتماد کنیم؟»

تمرین ۵ – کدنویسی و کدخوانی

۱۸. خروجی:

A
B
C

C().setup() اول setup کلاس C را اجرا می‌کند که در همان خط اول با super() به B می‌رود؛ B هم پیش از چاپ، با super() به A می‌رود. پس اول A چاپ می‌شود (انتهای زنجیره)، بعد کنترل به B برمی‌گردد و B چاپ می‌شود، و در آخر C. الگوی «اول والد، بعد خودم» — هر لایه اول کار والدش را کامل می‌کند.

۱۹.

class Temperature:
    ABSOLUTE_ZERO = -273.15

    def __init__(self, celsius=0):
        self._celsius = 0
        self.set_celsius(celsius)          # even __init__ goes through the gate

    def get_celsius(self):
        return self._celsius

    def set_celsius(self, value):
        if value < self.ABSOLUTE_ZERO:
            raise ValueError(f"temperature cannot be below {self.ABSOLUTE_ZERO}")
        self._celsius = value

t = Temperature(25)
print(t.get_celsius())    # 25
t.set_celsius(-100)
print(t.get_celsius())    # -100
# t.set_celsius(-300)     # ValueError: temperature cannot be below -273.15

نکته: حتی سازنده هم از set_celsius عبور می‌کند تا مسیر ورود مقدار دقیقاً یکی باشد و Temperature(-500) هم رد شود.

راه دیگر: پایتونیک‌ترین شکل همین طراحی، @property است که در فصل پنجم می‌بینید: کاربر t.celsius = -100 می‌نویسد و اعتبارسنجی به‌طور نامرئی اجرا می‌شود — بدون تغییر رابط.

۲۰.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        """every shape must implement this"""

class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return 3.14159 * self.r ** 2

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h

    def area(self):
        return self.w * self.h

class Triangle(Shape):
    def __init__(self, base, height):
        self.base, self.height = base, height

    def area(self):
        return self.base * self.height / 2

def describe_all(shapes):
    for shape in shapes:                   # no if/elif on types!
        print(f"{type(shape).__name__}: {shape.area()}")

describe_all([Circle(2), Rectangle(3, 4), Triangle(6, 5)])
# Circle: 12.56636
# Rectangle: 12
# Triangle: 15.0

افزودن شکل چهارم (مثلاً Square) فقط یعنی نوشتن یک کلاس جدید که از Shape ارث می‌برد و area را پیاده می‌کند؛ describe_all بدون هیچ تغییری با آن کار خواهد کرد، چون به قرارداد Shape تکیه دارد نه به فهرست انواع. (همان اصل Open/Closed که در فصل هشتم می‌آید.) ضمانت ABC هم این است که اگر Square متد area را جا بیندازد، در لحظه‌ی Square(...) خطای TypeError می‌گیرد، نه وسط describe_all.

راه دیگر: بدون ABC و فقط با Duck Typing هم describe_all کار می‌کند (کافی است هر کلاس area داشته باشد). برای یک تمرین کوچک، آن هم پذیرفتنی است؛ ABC وقتی می‌ارزد که بخواهید جاافتادن area را همان لحظه‌ی ساخت شیء کشف کنید.

۲۱. چون list(self._transactions) یک کپی می‌سازد و می‌فرستد؛ لیست اصلی درون کلاس محفوظ می‌ماند. اگر خود لیست برگردانده می‌شد، هر کد بیرونی به داده‌ی درونی کلاس ارجاع مستقیم داشت و مثلاً با این کد، بدون عبور از هیچ متدی، تاریخچه پاک می‌شد:

history = acc.get_history()
history.clear()               # with a shared reference, this would wipe
                              # the account's real transaction log!

یا حتی acc.get_history().append(("deposit", 999)) می‌توانست تراکنش جعلی ثبت کند. کپسول واقعی یعنی نه‌فقط attribute را _ کردن، بلکه ندادن ارجاع قابل‌دستکاری به داده‌ی درونی. (به این تکنیک، کپی دفاعی/Defensive Copy می‌گویند.)

تمرین ۶ – مقایسه و پرسش تحلیلی

۲۲. این جمله دو خطا دارد. اول، هدف را گم کرده: کپسول‌سازی ابزار محافظت از ناورداهاست، نه هدفی فی‌نفسه؛ attributeای که تغییر آزادش هیچ قانونی را نمی‌شکند، دلیلی برای پنهان‌شدن ندارد و خصوصی‌کردنش فقط کد را دست‌وپاگیر می‌کند (برای هر دسترسی، متد اضافه). دوم، ابزار را هم اشتباه انتخاب کرده: __ برای «قفل‌کردن» نیست؛ سازوکارش جلوگیری از تصادم نام در ارث‌بری است و به‌عنوان قفل هم واقعاً کار نمی‌کند (_Class__attr هنوز در دسترس است). قاعده‌ی سالم: پیش‌فرض _ برای اجزای داخلی؛ __ فقط وقتی کلاس برای ارث‌بری طراحی شده و attributeای دارید که هیچ زیرکلاسی نباید (حتی سهواً) با آن تداخل کند. Public هم برای چیزهایی که واقعاً جزء رابط‌اند — Public بودن، عیب نیست.

۲۳. مثال کلاسیک: Stack(list) — «پشته لازم دارم، لیست هم که همه‌ی متدها را دارد؛ پس ارث می‌برم!» نتیجه: پشته‌ای که insert(0, x) و sort() و ایندکس‌گذاری هم دارد؛ یعنی کل رابط ۴۰+ متدی لیست را به دوش می‌کشد و هر استفاده‌کننده‌ای می‌تواند قانون «فقط از بالا» را قانونی نقض کند. اصل «بازاستفاده با ارث‌بری» کورکورانه اجرا شده، در حالی که آزمون Is-A رد بود: پشته «یک نوع لیست» نیست (رابطش عمداً محدودتر است)؛ پشته با یک لیست ساخته می‌شود. راه درست، ترکیب بود:

class Stack:
    def __init__(self):
        self._items = []              # Has-A: composition

    def push(self, item):
        self._items.append(item)

    def pop(self):
        return self._items.pop()

s = Stack()
s.push(1)
s.push(2)
print(s.pop())    # 2
# s.insert, s.sort, s[0] ... simply do not exist — the interface stays honest

درس کلی: هر چهار اصل باید با سوال «این‌جا چه مشکلی را حل می‌کند؟» وارد کد شوند؛ اصلی که بدون پاسخ روشن به این سوال اعمال شود، معمولاً خودش مشکل بعدی است. (ادامه‌ی این بحث: Composition over Inheritance در فصل هفتم.)

Please login to bookmark Close
نظرات

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

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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