چند سال پیش، پروژه‌ای داشتم که به شکل عجیبی پیچیده شده بود: مدل داده‌ای با سی‌وشش کلید دیکشنری که بین ده‌ها تابع دست‌به‌دست می‌شد و در هر مرحله، کسی یک کلید جدید به آن اضافه می‌کرد. یک روز، بعد از سه ساعت دیباگ که چرا فیلد discount در یک مسیر خاص خالی است، تصمیم گرفتم همان دیکشنری را به یک کلاس کوچک پایتونی تبدیل کنم. سه ساعت بعد، کل کد تمیزتر شده بود و همان دسته باگ‌ها، دیگر قابل بازتولید نبودند. آن تجربه به من یاد داد که شی گرایی در پایتون فقط یک سبک برنامه‌نویسی نیست؛ ابزاری است که وقتی مسئله از یک حدی پیچیده‌تر می‌شود، از دیکشنری و تابع، به کلاس و متد مهاجرت می‌کنیم. در این مقاله، همان مسیری را می‌روم که با تازه‌کارها طی می‌کنم: از تعریف ساده‌ی کلاس تا وراثت، MRO، property، متدهای جادویی و dataclass.

چرا شی‌گرایی در پایتون؟

اگر با مفاهیم پایه‌ی پایتون آشنا نیستید، اول آموزش پایتون از صفر را بخوانید. اما فرض کنیم پایتون را می‌شناسید و می‌خواهید بفهمید چرا در پروژه‌های واقعی، بعد از چند هفته، دیکشنری و تابع کافی نیست. سه دلیل اصلی که در پروژه‌های خودم به آن‌ها رسیده‌ام:

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

نکته‌ی مهمی که پایتون را از زبان‌های شیءگرای کلاسیک مثل Java جدا می‌کند: در پایتون، همه‌چیز از ابتدا یک شیء است. عدد، رشته، تابع، ماژول — همه شیء هستند. یعنی حتی وقتی کلاس تعریف نمی‌کنید، در دنیای شیءها کار می‌کنید. این تفاوت را در ادامه، در مباحثی مثل متدهای جادویی، بیشتر خواهید دید. اگر با شی‌گرایی در زبان دیگری آشنا هستید، آموزش شی‌گرایی در PHP مفاهیم را با مثال‌های PHP توضیح می‌دهد که با کمی تطبیق، در پایتون هم مستقیم به‌کارتان می‌آید — فقط یادتان باشد که پایتون از ابتدا شیءگرا بود، در حالی که PHP مدل متفاوتی داشته است.

شی‌گرایی در پایتون، تصمیم برای زیبایی کد نیست؛ پاسخ به نقطه‌ای است که داده و منطق، از هم جدا نمی‌مانند.

کلاس و شیء: اولین قدم

ساده‌ترین کلاس پایتون، تنها یک نام دارد:

class User:
    pass

user = User()
print(type(user))      # <class "__main__.User">
print(user)            # <__main__.User object at 0x...>

نکته‌ی مهم: پایتون در این مرحله، هیچ تفاوتی بین دو شیء از یک کلاس قائل نمی‌شود — همه‌ی اشیا شبیه هم هستند تا وقتی ویژگی (attribute) به آن‌ها اضافه کنیم:

user.name = "Ali"
user.email = "ali@example.com"

print(user.name)  # Ali

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

سازنده و __init__

در پایتون، سازنده‌ی کلاس، متد __init__ است که هنگام ساخت شیء صدا می‌شود:

class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

user = User("Ali", "ali@example.com")
print(user.name)  # Ali

سه نکته‌ی مهم در این چند خط که در پروژه‌های واقعی به‌کارم آمده:

  • self اجباری است: برخلاف Java یا PHP که this خودکار اضافه می‌شود، در پایتون باید self را به‌عنوان اولین پارامتر همه‌ی متدها بنویسید. این ویژگی، خوانایی را بالا می‌برد چون همیشه صریح است — اما اگر از زبان دیگری آمده‌اید، چند بار فراموشش می‌کنید.
  • Type Hints: از پایتون ۳.۵ به بعد، می‌توانید نوع پارامترها را مشخص کنید. این کار، هم خوانایی را بالا می‌برد و هم ابزارهای بررسی مانند mypy می‌توانند خطاها را قبل از اجرا تشخیص دهند.
  • اعتبارسنجی در سازنده: اگر ورودی نامعتبر باشد، بهتر است همان‌جا استثنا پرتاب کنید نه این‌که شیء نامعتبر به دنیا بیاید. نمونه‌ی این کار را در مدیریت خطا در پایتون به‌عنوان الگوی Fail Fast توضیح داده‌ام.
class User:
    def __init__(self, name: str, email: str):
        if not name:
            raise ValueError("name cannot be empty")
        if "@" not in email:
            raise ValueError("invalid email")
        self.name = name
        self.email = email

متد نمونه، کلاس و استاتیک

پایتون سه نوع متد در کلاس دارد که در پروژه‌های واقعی، هرکدام جای خودشان را دارند:

۱) متد نمونه (Instance Method)

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

    def greet(self):
        return f"Hello, {self.name}"

معمول‌ترین نوع. اولین پارامتر self است و به نمونه‌ی کلاس دسترسی دارد.

۲) متد کلاس (Class Method)

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

    @classmethod
    def from_email(cls, email):
        name = email.split("@")[0]
        return cls(name)

اولین پارامتر cls است و به کلاس دسترسی دارد. کاربرد اصلی‌اش، سازنده‌های جانشین (alternative constructor) است. الگویی که در پروژه‌های واقعی بسیار به‌کارم آمده: ساخت شیء از روی داده‌ی خام (مثل رکورد دیتابیس یا پاسخ API):

class User:
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    @classmethod
    def from_db_row(cls, row):
        return cls(row["id"], row["name"], row["email"])

اگر با Django کار می‌کنید، این الگو همان چیزی است که در Managerها می‌بینید. آموزش جنگو برای مبتدیان نمونه‌های بیشتری از این الگو را نشان می‌دهد.

۳) متد استاتیک (Static Method)

class MathUtils:
    @staticmethod
    def is_even(n):
        return n % 2 == 0

نه به نمونه دسترسی دارد و نه به کلاس. وقتی استفاده می‌کنم که تابع، منطقاً متعلق به کلاس است ولی به هیچ‌کدام از self یا cls نیاز ندارد. قاعده‌ی من: اگر متد به هیچ‌کدام نیاز ندارد، به‌جای استاتیک، آن را به‌عنوان یک تابع ماژول جدا بنویسید — مگر این‌که واقعاً منطقاً بخشی از کلاس است.

نوع متدپارامتر اولکاربرد
نمونهselfمنطق مربوط به یک شیء خاص
کلاسclsسازنده‌های جانشین، تنظیمات کلاس
استاتیکنداردتوابع کمکی مرتبط با کلاس

وراثت و ارث‌بری چندگانه

وراثت در پایتون، شبیه اکثر زبان‌های شیءگرا است:

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

    def speak(self):
        raise NotImplementedError

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow"

ولی پایتون برخلاف Java یا PHP، از وراثت چندگانه پشتیبانی می‌کند:

class Swimmer:
    def swim(self):
        return "Swimming"

class Flyer:
    def fly(self):
        return "Flying"

class Duck(Animal, Swimmer, Flyer):
    def speak(self):
        return f"{self.name} says Quack"

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

  • فقط برای mixin: کلاس‌هایی که قصد دارند رفتار مشخصی را اضافه کنند، مثل Swimmer، نه این‌که ساختار اصلی را تعریف کنند.
  • ترتیب مهم است: ترتیب والدها در تعریف کلاس، روی رفتار نهایی اثر می‌گذارد — این همان MRO است که در بخش بعدی می‌گوییم.
  • ترجیح ترکیب بر وراثت: اگر به‌جای «هست» از «دارد» استفاده می‌کنید، ترکیب انتخاب درست است. در بخش ترکیب یا وراثت به این برمی‌گردیم.

MRO و super()

وقتی یک کلاس از چند والد ارث می‌برد، ترتیب جستجوی متدها مهم می‌شود. پایتون از الگوریتمی به نام C3 استفاده می‌کند که MRO (Method Resolution Order) تولید می‌کند:

print(Duck.__mro__)
# (<class "Duck">, <class "Animal">, <class "Swimmer">, <class "Flyer">, <class "object">)

این خروجی می‌گوید پایتون، وقتی به‌دنبال یک متد در Duck می‌گردد، اول خود کلاس، بعد Animal، بعد Swimmer، بعد Flyer و در نهایت object را بررسی می‌کند. این ترتیب، از چپ به راست والدها در تعریف کلاس پیروی می‌کند.

super(): فراخوانی والد بدون نام‌بردن

در پایتون ۳، super() بدون پارامتر، والد بعدی در MRO را صدا می‌زند:

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

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

مزیت این الگو، در وراثت چندگانه خودش را نشان می‌دهد: اگر Dog به super().__init__ صدا بزند و والد Animal هم همین کار را بکند، کل زنجیره به‌درستی اجرا می‌شود. اگر به‌جای super از Animal.__init__(self, name) استفاده کنید، زنجیره می‌شکند و والدهای دیگر نادیده گرفته می‌شوند.

در وراثت چندگانه، super() دوست شماست و فراخوانی مستقیم با نام والد، دشمن پنهان. تفاوت در یک خط کد است ولی در بودجه‌ی دیباگ، ساعت‌ها فاصله دارد.

یک نکته‌ی ظریف در MRO که در پروژه‌های واقعی به‌کارم آمده: اگر ترتیب والدها را عوض کنید، رفتار کلاس می‌تواند به‌کلی متفاوت شود. به همین دلیل، در وراثت چندگانه، همیشه یک تست کوچک بنویسید که __mro__ را چاپ کند و ببینید ترتیب واقعاً همان است که انتظار دارید. تجربه‌ی من: در پروژه‌ای که سه mixin روی هم داشتند، دو ساعت دیباگ کردم تا بفهمم چرا یک متد override نمی‌شود — فقط برای این‌که بفهمم ترتیب mixinها را اشتباه نوشته‌ام. اصول کلی وراثت، در بستر زبان‌های دیگر هم مشابه است؛ برای نمونه آموزش شی‌گرایی در PHP این تفاوت‌ها را کنار هم می‌گذارد.

کپسوله‌سازی و نام‌گذاری داخلی

پایتون برخلاف Java، مفهوم private واقعی ندارد. بلکه از یک قرارداد نام‌گذاری استفاده می‌کند:

  • name: عمومی. هر کسی می‌تواند مستقیم دسترسی داشته باشد.
  • _name: داخلی. قرارداد می‌گوید از بیرون استفاده نکنید، ولی از نظر فنی اجازه دارید.
  • __name: خصوصی با نام‌گذاری دو زیرخط. پایتون نام را به _ClassName__name تغییر می‌دهد (name mangling) که از دسترسی ناخواسته در وراثت جلوگیری می‌کند.
class Account:
    def __init__(self, balance):
        self._balance = balance      # داخلی
        self.__pin = "1234"          # name-mangled

    def get_balance(self):
        return self._balance

فلسفه‌ی پایتون این است: «ما همه بزرگسال هستیم». یعنی اگر شما نمی‌خواهید کسی از یک ویژگی استفاده کند، با _ علامتش می‌زنید و به بقیه اعتماد می‌کنید. این رویکرد، هم انعطاف‌پذیرتر است و هم در تست و دیباگ، سریع‌تر به‌کار می‌آید — ولی در پروژه‌های تیمی بزرگ، اگر همه به قرارداد احترام نگذارند، به آشفتگی می‌رسد. تجربه‌ی من: در پروژه‌های تیمی، علاوه بر _، حتماً در مستندسازی هم بنویسید کدام ویژگی داخلی است و چرا — قرارداد شفاهی، در تیم‌های بزرگ به‌سرعت فراموش می‌شود.

Property و descriptor

در بعضی موارد، می‌خواهید دسترسی به یک ویژگی، منطق اضافه داشته باشد — مثلاً اعتبارسنجی یا محاسبه‌ی تنبل. در زبان‌های دیگر، این کار با getter و setter انجام می‌شود:

# الگوی getter/setter (نه پایتونیک)
class Circle:
    def __init__(self, radius):
        self._radius = radius

    def get_radius(self):
        return self._radius

    def set_radius(self, value):
        if value < 0:
            raise ValueError
        self._radius = value

ولی پایتون راه تمیزتری دارد: دکوراتور @property:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

مزیت این الگو در پروژه‌های واقعی، دو چیز است:

  • سازگاری عقب‌رو: می‌توانید یک ویژگی عمومی را بعداً به property تبدیل کنید، بدون این‌که کد مصرف‌کننده تغییر کند. circle.radius هنوز کار می‌کند، فقط حالا اعتبارسنجی هم دارد.
  • ویژگی محاسباتی: area در واقع فیلد ذخیره‌شده نیست، ولی از دید کاربر، مثل یک ویژگی به‌نظر می‌رسد. این ویژگی در مدل‌های داده‌ای بسیار مفید است — اگر با کتابخانه pandas در پایتون کار کرده باشید، همین الگو را در ستون‌های محاسباتی دیده‌اید.

یک نکته‌ی ظریف درباره‌ی @property که در پروژه‌های واقعی اهمیت دارد: اگر ویژگی محاسباتی سنگین است (مثلاً چند کوئری دیتابیس)، به‌جای property، از یک متد ساده استفاده کنید. دلیلش این است که کد مصرف‌کننده، obj.total_cost را به‌عنوان دسترسی سریع می‌بیند، ولی پشت صحنه ممکن است یک کوئری سنگین باشد. نگه‌داشتن این تفکیک، از باگ‌های کارایی جلوگیری می‌کند.

# بد: property با هزینه‌ی پنهان
@property
def total_orders(self):
    return Order.objects.filter(user=self).count()  # کوئری در هر دسترسی

# بهتر: متد صریح
def get_total_orders(self):
    return Order.objects.filter(user=self).count()

متدهای جادویی (dunder)

متدهای جادویی یا dunder (double underscore)، رفتارهای شیء را در عملیات زبان تعریف می‌کنند. از پایتون ۳ به بعد، این‌ها قلب طراحی پایتونیک هستند. چند نمونه که در پروژه‌های واقعی بارها استفاده می‌کنم:

__str__ و __repr__

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def __repr__(self):
        return f"User(name={self.name!r}, email={self.email!r})"

    def __str__(self):
        return f"{self.name} <{self.email}>"

تفاوت __repr__ و __str__ در پروژه‌های واقعی مهم است: __repr__ برای توسعه‌دهنده (خروجی در کنسول و دیباگ) و __str__ برای کاربر نهایی. اگر فقط یکی را بنویسید، __repr__ را بنویسید — چون در نبود __str__، پایتون از __repr__ استفاده می‌کند ولی برعکسش نه.

__eq__ و __hash__

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

اگر __eq__ را تعریف کنید ولی __hash__ را فراموش کنید، شیء شما در set و dict غیرقابل استفاده می‌شود. این یک باگ کلاسیک در پروژه‌های واقعی است که معمولاً تا لحظه‌ای که یک set تعریف می‌کنید، خودش را نشان نمی‌دهد.

__iter__ و __next__

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

for n in Countdown(3):
    print(n)  # 3, 2, 1

این دو متد، شیء شما را قابل پیمایش با for می‌کنند. در پروژه‌های واقعی، وقتی با داده‌های حجیم کار می‌کنید، این الگو جایگزین بهتری برای بارگذاری کل داده در لیست است. نمونه‌ی مکملش در کار با فایل‌های بزرگ در کار با فایل‌ها در پایتون با پیمایش خط‌به‌خط آمده است.

__enter__ و __exit__

class Timer:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.perf_counter() - self.start
        print(f"{self.label}: {elapsed:.3f}s")

with Timer("Processing"):
    process_data()

این دو متد، شیء شما را به یک context manager تبدیل می‌کنند. الگویی که در پروژه‌های واقعی بسیار به‌کارم آمده — همان مفهوم را در مدیریت خطا در پایتون با جزئیات بیشتر توضیح داده‌ام.

فهرست کامل dunderها طولانی است. ولی در پروژه‌های واقعی، همان چند نمونه‌ی بالا، ۹۰٪ نیازها را پوشش می‌دهد. یادگیری بقیه، وقتی لازمی می‌شود که با کتابخانه‌ای خاص کار می‌کنید یا API طراحی می‌کنید که مصرف‌کننده‌هایش به رفتار پایتونیک خاصی نیاز دارند.

dataclass: جانشین مدرن کلاس‌های داده

از پایتون ۳.۷ به بعد، دکوراتور @dataclass کار ساخت کلاس‌های داده را به‌شدت ساده کرده. مقایسه کنید:

# بدون dataclass
class User:
    def __init__(self, name, email, age=0):
        self.name = name
        self.email = email
        self.age = age

    def __repr__(self):
        return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.email, self.age) == (other.name, other.email, other.age)

و با dataclass:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int = 0

خروجی، تقریباً یکسان است ولی کد بسیار کوتاه‌تر. @dataclass به‌طور خودکار __init__، __repr__ و __eq__ را می‌سازد. در پروژه‌های واقعی، از زمانی که این ویژگی را یاد گرفتم، اکثر کلاس‌های داده‌ای‌ام را به dataclass تبدیل کرده‌ام. چند نکته‌ی کلیدی:

  • مقدار پیش‌فرض: پارامترهای بدون مقدار پیش‌فرض باید قبل از پارامترهای با پیش‌فرض بیایند.
  • Mutable default: برای لیست یا دیکشنری به‌عنوان مقدار پیش‌فرض، از field(default_factory=list) استفاده کنید — نه [] مستقیم.
  • frozen=True: اگر می‌خواهید شیء غیرقابل تغییر باشد، از @dataclass(frozen=True) استفاده کنید. این ویژگی برای مقدارهای ثابت و کلیدهای dictionary ایده‌آل است.
  • slots=True (پایتون ۳.۱۰+): برای کاهش مصرف حافظه در پروژه‌هایی که هزاران نمونه می‌سازند.
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Order:
    id: int
    items: list = field(default_factory=list)
    total: float = 0.0

در پروژه‌های واقعی، تفاوت بین کلاس داده‌ی دستی و dataclass، در خوانایی و قابلیت نگهداری است. کدی که هفتاد خط __init__ و __repr__ و __eq__ دارد، به سه خط تبدیل می‌شود و این تفاوت، در بازبینی کد و دیباگ، محسوس است.

dataclass، نه به‌معنی کلاس کمتر، بلکه به‌معنی کلاس شفاف‌تر است؛ اگر کلاس شما فقط داده نگه می‌دارد، dataclass انتخاب درست است.

ترکیب یا وراثت؟

یک بحث همیشگی در شیءگرایی که جواب روشنی در پروژه‌های واقعی دارد. قاعده‌ی کلاسیک: «ترکیب را به وراثت ترجیح بده.» ولی چرا؟

وراثت، رابطه‌ی «هست» (is-a) را نشان می‌دهد. Dog یک Animal است. ترکیب، رابطه‌ی «دارد» (has-a) را نشان می‌دهد. Car یک Engine دارد.

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

# وراثت (سخت‌تر تغییر می‌کند)
class Report(DatabaseLogger):
    pass

# ترکیب (انعطاف‌پذیرتر)
class Report:
    def __init__(self, logger):
        self.logger = logger

در تجربه‌ی من، وراثت جایی مناسب است که سلسله‌مراتب واقعاً «هست» باشد — مثلاً Circle یک Shape است. در بقیه‌ی موارد، ترکیب انتخاب بهتری است. یکی از نکات مهمی که در بازبینی کد پروژه‌های تازه‌کار زیاد دیده‌ام: وراثت عمیق (سه سطح یا بیشتر) به‌سرعت به کابوس نگهداری تبدیل می‌شود. قاعده‌ی من: حداکثر دو سطح وراثت در پروژه‌های تجاری. اگر بیشتر از این شد، احتمالاً ترکیب انتخاب درست‌تری است. اصول مشابهش در آموزش شی‌گرایی در PHP هم صادق است — این قاعده، اختصاصی به پایتون ندارد.

اشتباهات رایج در شی‌گرایی پایتون

در بازبینی پروژه‌های پایتونی، این اشتباهات را زیاد دیده‌ام:

  • استفاده‌ی بیش‌ازحد از کلاس: هر تابع را به کلاس تبدیل می‌کنند. گاهی یک تابع ساده، بهتر از کلاس با متد است. اگر کلاس فقط یک متد دارد، احتمالاً تابع کافی است.
  • وراثت عمیق: پنج سطح وراثت به‌جای ترکیب. نیمی از زمان دیباگ در چنین پروژه‌هایی، برای فهمیدن این است که متد در کدام سطح تعریف شده.
  • نبود __repr__: وقتی در لاگ یا دیباگ، خروجی <User object at 0x...> می‌بینید، نمی‌دانید این کدام کاربر است. همیشه __repr__ بنویسید.
  • تعریف __eq__ بدون __hash__: باعث می‌شود شیء در set یا dict غیرقابل استفاده شود.
  • استفاده از __del__: پایتون تضمین نمی‌کند __del__ چه‌وقت اجرا می‌شود. برای آزادسازی منابع، از __exit__ یا weakref استفاده کنید.
  • attributeهای داینامیک: اضافه‌کردن ویژگی به شیء در جای دلخواه کد. الگوی درست، تعریف همه در __init__ است — یا استفاده از __slots__ که جلوی افزودن ویژگی خارج از فهرست را می‌گیرد.
  • کلاس‌های بزرگ: کلاسی که صدها خط و ده‌ها متد دارد. قانون سرانگشتی من: اگر کلاس از ۲۰۰ خط عبور کرد، احتمالاً باید به دو یا سه کلاس تقسیم شود. این تقسیم، در بلندمدت، تعمیر و گسترش را چند برابر آسان‌تر می‌کند.

یک نکته‌ی مهم از تجربه: قبل از این‌که یک کلاس جدید بسازید، سه سؤال از خودتان بپرسید: آیا داده‌ای که نگه می‌دارد، بیش از پنج فیلد است؟ آیا منطق مرتبط با آن داده، بیش از دو تابع است؟ آیا این داده در چند جای پروژه استفاده می‌شود؟ اگر جواب هر سه «نه» است، دیکشنری و تابع کافی است. اگر حداقل دو تا «بله» است، کلاس انتخاب درست است. این سه سؤال، جلوی بسیاری از کلاس‌های اضافی را که در پروژه‌ها می‌بینم، می‌گیرد.

سخن آخر

شی‌گرایی در پایتون، از یک کلاس ساده‌ی User شروع می‌شود ولی در پروژه‌های واقعی، به یک تصمیم معماری تبدیل می‌شود که کیفیت کد شما را در بلندمدت تعیین می‌کند. سه نکته‌ی مهم که در این مقاله به آن‌ها رسیدیم: اول، پایتون از ابتدا شیءگرا است — این ویژگی، متدهای جادویی و رفتار انواع داده را متفاوت از زبان‌های شیءگرای کلاسیک می‌کند؛ دوم، وراثت چندگانه و MRO، قدرتمند ولی پرخطرند — با super() زنجیره را حفظ کنید و از وراثت عمیق پرهیز کنید؛ سوم، dataclass و property، ابزارهای مدرنی هستند که کلاس‌های داده‌ای و منطق اعتبارسنجی را به‌شدت ساده می‌کنند — از آن‌ها در پروژه‌های جدید بهره ببرید.

اگر امروز می‌خواهید شروع کنید، سه کار کوچک پیشنهاد می‌کنم: یک دیکشنری که در پروژه‌تان مدام دست‌به‌دست می‌شود را به dataclass تبدیل کنید، برای یک کلاس موجود __repr__ بنویسید، و اگر جایی وراثت بیش از دو سطح دارید، بخشی از آن را با ترکیب بازنویسی کنید. همین سه کار، کیفیت کد شما را در نگاه اول بالا می‌برد. اگر تجربه‌ای از شی‌گرایی در پروژه‌های خودتان دارید — مخصوصاً اگر با MRO یا وراثت چندگانه به مشکل برخورده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🐍