شی گرایی در پایتون
شیگرایی در پایتون با بقیه زبانها یک تفاوت جدی دارد: همهچیز از ابتدا شیء است. از تعریف کلاس و __init__ تا وراثت چندگانه، MRO، property، متدهای جادوی
چند سال پیش، پروژهای داشتم که به شکل عجیبی پیچیده شده بود: مدل دادهای با سیوشش کلید دیکشنری که بین دهها تابع دستبهدست میشد و در هر مرحله، کسی یک کلید جدید به آن اضافه میکرد. یک روز، بعد از سه ساعت دیباگ که چرا فیلد 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 یا وراثت چندگانه به مشکل برخوردهاید — در دیدگاهها بنویسید؛ همین نکتههای میدانی، برای خوانندهی بعدی از هر مستند رسمی ارزشمندتر است. 🐍