فصل ۷: ماژول‌ها، کتابخانه‌ی استاندارد و شیءگرایی

وراثت، dunder methods (__str__، __repr__، __eq__) و dataclass

کلاس‌هایی که با پایتون هماهنگ‌اند

وراثت

وراثت یعنی کلاس جدید، همه‌چیز کلاس والد را به ارث ببرد و فقط تفاوت‌ها را تعریف کند. فرش ابریشمی همان فرش است، با یک ویژگی اضافه و قیمت‌گذاری متفاوت:

class SilkRug(Rug):
    def __init__(self, code, width_cm, length_cm, price_per_sqm, raj: int):
        super().__init__(code, width_cm, length_cm, price_per_sqm)   # کار والد را انجام بده
        self.raj = raj

    def total_price(self) -> int:
        base = super().total_price()
        return round(base * 1.4) if self.raj >= 60 else base

s = SilkRug("KSH-777", 150, 225, 20_000_000, raj=70)
print(isinstance(s, Rug))       # True — فرش ابریشمی، فرش هم هست

وراثت را فقط برای رابطه‌ی واقعی «... یک نوع ... است» به کار ببرید. برای «... یک ... دارد» (سفارش یک مشتری دارد) از ترکیب استفاده کنید: شیء مشتری را به‌عنوان ویژگی در سفارش بگذارید. سلسله‌مراتب عمیق وراثت، کد را شکننده می‌کند.

متدهای dunder

متدهایی که با دو زیرخط شروع و تمام می‌شوند (double underscore، «dunder») به کلاس شما اجازه می‌دهند با عملگرها و توابع داخلی پایتون کار کند:

class Rug:
    # ... __init__ و بقیه مثل درس قبل
    def __repr__(self) -> str:          # برای برنامه‌نویس: دیباگ، لاگ، REPL
        return f"Rug({self.code!r}, {self.width_cm}, {self.length_cm})"

    def __str__(self) -> str:           # برای کاربر: print و f-string
        return f"فرش {self.code} — {self.area:g} متر مربع"

    def __eq__(self, other) -> bool:    # عملگر ==
        if not isinstance(other, Rug):
            return NotImplemented
        return self.code == other.code
متدبا چه چیزی فعال می‌شود
__str__print(r)، str(r)، f-string
__repr__REPL، نمایش داخل لیست، repr(r)، {r!r}
__eq__== و in
__lt__< و در نتیجه sorted
__len__len(obj) و truthiness

اگر فقط یکی را تعریف می‌کنید، __repr__ باشد؛ print در نبود __str__ از آن استفاده می‌کند.

dataclass: کلاس داده بدون کد تکراری

نوشتن __init__، __repr__ و __eq__ برای هر کلاس داده خسته‌کننده است. دکوراتور @dataclass همه را از روی فیلدهای annotate‌شده خودکار می‌سازد:

from dataclasses import dataclass, field, asdict

@dataclass
class Order:
    code: str
    customer: str
    area: float
    price: int
    tags: list[str] = field(default_factory=list)   # نه tags: list = []
    status: str = "new"

    def __post_init__(self):
        if self.price <= 0:
            raise ValueError("قیمت نامعتبر")

o = Order("KSH-101", "مریم کاشانی", 6, 57_000_000)
print(o)             # Order(code='KSH-101', customer='مریم کاشانی', ..., status='new')
print(asdict(o))     # dict آماده برای json.dumps
print(o == Order("KSH-101", "مریم کاشانی", 6, 57_000_000))   # True

@dataclass(frozen=True, order=True)
class Size:
    width: int
    length: int      # تغییرناپذیر، قابل مرتب‌سازی و قابل استفاده به‌عنوان کلید dict

نکته‌هایی که کمتر کسی می‌داند

  • تعریف __eq__ بدون __hash__، شیء را unhashable می‌کند: دیگر نمی‌توانید آن را در set یا به‌عنوان کلید dict بگذارید. frozen=True در dataclass این مشکل را حل می‌کند.
  • برگرداندن NotImplemented (نه False) در __eq__ به پایتون اجازه می‌دهد مقایسه را از طرف دیگر امتحان کند؛ خود NotImplemented یک مقدار ویژه است، نه استثنا.
  • field(default_factory=list) همان راه‌حل دام «پیش‌فرض mutable» در فصل ۵ است؛ dataclass اصلاً اجازه‌ی tags: list = [] را نمی‌دهد و ValueError می‌دهد.
  • Order(**row) یک dict خوانده‌شده از JSON یا CSV را مستقیم به dataclass تبدیل می‌کند، به شرط این‌که کلیدها با نام فیلدها یکی باشند.
  • @dataclass(slots=True) (3.10+) مصرف حافظه‌ی هر شیء را کم می‌کند و غلط تایپی در نام ویژگی (o.custmer = ...) را به خطا تبدیل می‌کند.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.