متدهای جادویی و استثناهای سفارشی
چرا len روی لیست کار میکند و روی کلاس شما نه؟ چرا
میشود دو رشته را با + جمع کرد؟ در این فصل رازش را
میفهمید و کلاسهایتان را مثل نوعهای خود پایتون میکنید.
۱. متد جادویی چیست
متدهایی که نامشان با دو زیرخط شروع و تمام میشود، متد جادویی یا dunder نام دارند. شما آنها را صدا نمیزنید؛ پایتون در موقعیتهای خاص خودش صدایشان میزند.
| وقتی مینویسید | پایتون صدا میزند |
|---|---|
Person('Ali') | __init__ |
print(p) | __str__ |
len(p) | __len__ |
a + b | __add__ |
a == b | __eq__ |
x in p | __contains__ |
p[0] | __getitem__ |
for x in p | __iter__ |
with p: | __enter__ و __exit__ |
۲. __str__ و __repr__
# Without __str__, printing an object gives something useless like
# <__main__.Human object at 0x000001C3...>
class Human:
def __init__(self, name: str = 'Adam', lastname: str = ''):
self.name = name
self.lastname = lastname
def __str__(self) -> str:
'''Used by print() and str(): a friendly text for the USER.'''
return f'{self.name} {self.lastname}'.strip()
def __repr__(self) -> str:
'''Used in the interpreter, in a list, and while debugging:
a precise text for the DEVELOPER.'''
return f'Human(name={self.name!r}, lastname={self.lastname!r})'
if __name__ == '__main__':
h = Human('Kasra', 'Bagheri')
print(h) # __str__
print(repr(h)) # __repr__
print([h]) # a list uses __repr__ for its items!
print(f'{h}') # __str__
print(f'{h!r}') # __repr__
دو نمایش متنی: نمونه 01_str_and_repr.py
Kasra Bagheri Human(name='Kasra', lastname='Bagheri') [Human(name='Kasra', lastname='Bagheri')] Kasra Bagheri Human(name='Kasra', lastname='Bagheri')
__str__ | __repr__ | |
|---|---|---|
| برای چه کسی | کاربر نهایی | برنامهنویس |
| چه شکلی | خوانا و ساده | دقیق، ترجیحاً شبیه کد ساخت شیء |
| کجا | print، str()، f-string | مفسر، داخل لیست، عیبیابی |
__repr__ را بنویسید. اگر __str__ نباشد،
پایتون به __repr__ پناه میبرد. برعکسش صادق نیست، و
بدون __repr__، اشیاء داخل لیست به شکل بیفایده
<object at 0x...> چاپ میشوند.
۳. len و bool و in
class Playlist:
def __init__(self, name: str) -> None:
self.name = name
self.songs: list[str] = []
def add(self, song: str) -> None:
self.songs.append(song)
def __len__(self) -> int:
'len(playlist)'
return len(self.songs)
def __bool__(self) -> bool:
'''bool(playlist) and "if playlist:".'''
return len(self.songs) > 0
def __contains__(self, song: str) -> bool:
'"song in playlist"'
return song in self.songs
def __str__(self) -> str:
return f'{self.name} ({len(self)} songs)'
if __name__ == '__main__':
playlist = Playlist('Study')
if not playlist:
print('The playlist is empty.')
playlist.add('Song A')
playlist.add('Song B')
print(len(playlist)) # 2
print('Song A' in playlist) # True
print(playlist)
رفتار ظرفمانند: نمونه 02_len_bool_contains.py
با سه متد ساده، کلاس شما در دستور if، در
len و با عملگر in طبیعی رفتار میکند. نکته:
اگر __bool__ نباشد پایتون از __len__
استفاده میکند و اگر آن هم نباشد، هر شیئی همیشه True
حساب میشود.
۴. مقایسه و مرتبسازی
from functools import total_ordering
@total_ordering # generates <=, >, >= out of __eq__ and __lt__
class Student:
def __init__(self, name: str, score: float) -> None:
self.name = name
self.score = score
def __repr__(self) -> str:
return f'Student({self.name!r}, {self.score})'
def __eq__(self, other) -> bool:
# Always check the type: comparing with something else must not crash.
if not isinstance(other, Student):
return NotImplemented
return self.score == other.score
def __lt__(self, other) -> bool:
if not isinstance(other, Student):
return NotImplemented
return self.score < other.score
def __hash__(self) -> int:
'''Defining __eq__ removes the default __hash__, so the object can no
longer go into a set or be a dict key. We give it back here.'''
return hash(self.score)
if __name__ == '__main__':
a = Student('Ali', 18)
b = Student('Sara', 20)
c = Student('Reza', 18)
print(a == c) # True - same score
print(a < b) # True
print(b >= a) # True - generated by total_ordering
students = [b, a, c]
print(sorted(students))
print(max(students))
مقایسه: نمونه 03_comparison.py
شرح مثال
- با تعریف
__eq__و__lt__، دیگر ضرورتی ندارد موقعsortedپارامترkeyبدهید؛ خود کلاس میداند چطور مقایسه شود. - دکوراتور
@total_orderingچهار عملگر دیگر را از روی همین دو تا میسازد. - برگرداندن
NotImplementedوقتی نوع طرف مقابل جور نیست، کار درست است: به پایتون میگوید «من بلد نیستم، تو تصمیم بگیر». - نکته ظریف: به محض تعریف
__eq__، پایتون__hash__پیشفرض را حذف میکند و شیء دیگر داخل مجموعه یا کلید دیکشنری نمیرود. اگر لازمش دارید باید خودتان تعریفش کنید.
۵. سربارگذاری عملگرهای ریاضی
class Money:
def __init__(self, amount: float, currency: str = 'Toman') -> None:
self.amount = amount
self.currency = currency
def __str__(self) -> str:
return f'{self.amount:,.0f} {self.currency}'
def __add__(self, other: 'Money') -> 'Money':
'''Called by a + b.
IMPORTANT: it must RETURN a value, not print one.'''
if not isinstance(other, Money) or other.currency != self.currency:
return NotImplemented
return Money(self.amount + other.amount, self.currency)
def __mul__(self, factor: float) -> 'Money':
'Called by money * 3'
return Money(self.amount * factor, self.currency)
def __rmul__(self, factor: float) -> 'Money':
'Called by 3 * money (the "reflected" version)'
return self * factor
def __neg__(self) -> 'Money':
'Called by -money'
return Money(-self.amount, self.currency)
if __name__ == '__main__':
price = Money(15_000)
tax = Money(1_350)
print(price + tax)
print(price * 3)
print(3 * price) # works thanks to __rmul__
print(-price)
# A different currency is refused by Python itself:
try:
print(price + Money(10, 'Euro'))
except TypeError as e:
print('Refused:', e)
عملگرهای ریاضی: نمونه 04_arithmetic.py
در فایل اصلی، متد __add__ نتیجه را
چاپ میکرد به جای اینکه برگرداند، و
__eq__ هم چیزی برنمیگرداند یعنی None
میداد. نتیجه این بود که a + b برابر None
میشد و a == b بیسروصدا همیشه نادرست بود. قانون:
متد جادویی ریاضی همیشه باید یک شیء
تازه برگرداند.
نکته __rmul__: وقتی مینویسید 3 * price،
پایتون اول از عدد ۳ میپرسد که آیا میداند چطور در یک
Money ضرب شود. جواب منفی است، پس سراغ نسخه
آینهای در کلاس شما میآید.
۶. اندیس و پیمایش
class Deck:
def __init__(self) -> None:
self.cards = [f'{value}{suit}'
for suit in 'SHDC'
for value in ['A', '2', '3', 'J', 'Q', 'K']]
def __len__(self) -> int:
return len(self.cards)
def __getitem__(self, index):
'deck[0] and even deck[0:3]'
return self.cards[index]
def __setitem__(self, index, value) -> None:
'deck[0] = ...'
self.cards[index] = value
def __iter__(self):
'for card in deck'
return iter(self.cards)
if __name__ == '__main__':
deck = Deck()
print(deck[0])
print(deck[0:3]) # slicing works for free
for card in deck:
print(card, end=' ')
print()
# Once __iter__ exists, everything that loops works: in, list(), sorted()...
print('QH' in deck)
print(list(deck)[:5])
اندیس و پیمایش: نمونه 05_indexing_and_iteration.py
با تعریف __iter__، ناگهان همه ابزارهایی که با حلقه کار
میکنند روی کلاس شما هم کار میکنند: in،
list()، sorted()، sum()، درک
لیستی و همه چیز. این قدرت واقعی متدهای جادویی است.
۷. شیء قابل صدا زدن و مدیر زمینه
# __call__ makes an OBJECT behave like a function.
class Multiplier:
def __init__(self, factor: float) -> None:
self.factor = factor
def __call__(self, value: float) -> float:
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(10)) # 20 - the object is called like a function
print(callable(double)) # True
شیء قابل صدا زدن: نمونه 06_call_and_context.py
# __enter__ / __exit__ let us write our own 'with' block.
class Timer:
'Measure how long the block inside "with" takes.'
def __init__(self, label: str = 'block') -> None:
self.label = label
def __enter__(self):
import time
self.start = time.perf_counter()
print(f'[{self.label}] started')
return self # what "as t" receives
def __exit__(self, exc_type, exc_value, traceback):
import time
self.elapsed = time.perf_counter() - self.start
print(f'[{self.label}] took {self.elapsed:.4f} s')
# Returning False (or None) lets an exception propagate normally.
return False
if __name__ == '__main__':
with Timer('sum'):
total = sum(range(2_000_000))
مدیر زمینه: نمونه 06_call_and_context.py
شرح مثال
- حالا میفهمید
with open(...)فصل هجدهم چطور کار میکرد: فایل هم یک مدیر زمینه است. __enter__در ابتدای بلوک اجرا میشود و هر چه برگرداند به متغیر بعد ازasمیرسد.__exit__در پایان اجرا میشود، حتی اگر داخل بلوک خطا رخ دهد. سه پارامترش اطلاعات آن خطا هستند.- برگرداندن
Falseیعنی «خطا را قورت نمیدهم، بگذار بالا برود». اگرTrueبرگردانید، خطا خفه میشود که معمولاً کار بدی است.
۸. نمونه کامل: بردار
'''A 2-D vector class that uses many dunder methods at once.'''
import math
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
# --- text representations ---
def __str__(self) -> str:
return f'({self.x}, {self.y})'
def __repr__(self) -> str:
return f'Vector({self.x}, {self.y})'
# --- arithmetic ---
def __add__(self, other: 'Vector') -> 'Vector':
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, factor: float) -> 'Vector':
return Vector(self.x * factor, self.y * factor)
def __rmul__(self, factor: float) -> 'Vector':
return self * factor
def __neg__(self) -> 'Vector':
return Vector(-self.x, -self.y)
# --- comparison ---
def __eq__(self, other) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self) -> int:
return hash((self.x, self.y))
# --- built-in functions ---
def __abs__(self) -> float:
'abs(v) returns the length of the vector.'
return math.sqrt(self.x ** 2 + self.y ** 2)
def __getitem__(self, index: int) -> float:
'v[0] is x, v[1] is y.'
return (self.x, self.y)[index]
def __iter__(self):
'Allows x, y = v'
yield self.x
yield self.y
if __name__ == '__main__':
a = Vector(3, 4)
b = Vector(1, 2)
print(a + b)
print(a * 2, 2 * a)
print(abs(a)) # 5.0
x, y = a # unpacking, thanks to __iter__
print(x, y)
کلاس بردار: نمونه 07_vector.py
این کلاس دقیقاً مثل یک نوع درونی پایتون رفتار میکند: جمع میشود، ضرب
میشود، طولش با abs گرفته میشود، باز میشود و در مجموعه
مینشیند. کلمه yield در __iter__ موضوع فصل
بیست و ششم است.
۹. اولین استثنای سفارشی
# An exception is just a class that inherits from Exception.
class LoginError(Exception):
'Raised when a login attempt fails.'
def login(username: str, password: str) -> str:
if username != 'admin':
raise LoginError('Username is incorrect')
if password != '1234':
raise LoginError('Password is incorrect')
return 'Logged in!'
if __name__ == '__main__':
try:
print(login('admin', 'wrong'))
except LoginError as e:
print('Error:', e)
استثنای سفارشی: نمونه 08_custom_exception.py
چرا استثنای خودمان
- ساختنش فقط یک خط است: کلاسی که از
Exceptionارث میبرد. - صداکننده میتواند دقیقاً خطای برنامه شما را بگیرد، نه یک
ValueErrorکلی که ممکن است از هر جای دیگری آمده باشد. - نام خطا خودش مستندسازی است:
LoginErrorبدون هیچ توضیحی معلوم است چیست. - چون از
Exceptionارث میبرد،except Exceptionهم آن را میگیرد.
۱۰. استثنایی که داده حمل میکند
# An exception is an object, so it can carry extra data - a code, a field name,
# the value that was refused...
class AppError(Exception):
def __init__(self, message: str, code: int) -> None:
super().__init__(message) # let Exception store the message
self.code = code # our own extra data
def withdraw(balance: float, amount: float) -> float:
if amount <= 0:
raise AppError('The amount must be more than zero.', code=1001)
if amount > balance:
raise AppError('Not enough money.', code=1002)
return balance - amount
if __name__ == '__main__':
try:
withdraw(200, -1000)
except AppError as e:
print(f'Error [{e.code}]: {e}')
# The code lets the caller react differently to each situation,
# and lets the message be translated without touching the logic.
MESSAGES = {
1001: 'The amount you entered is not valid.',
1002: 'Your balance is too low.',
}
try:
withdraw(100, 500)
except AppError as e:
print(MESSAGES.get(e.code, 'Unknown error.'))
استثنا با کد: نمونه 10_exception_with_code.py
استثنا هم یک شیء است، پس میتواند صفت داشته باشد. کد خطا به شما اجازه میدهد پیامها را جدا نگه دارید و مثلاً به فارسی ترجمهشان کنید، بدون اینکه به منطق برنامه دست بزنید.
۱۱. سلسلهمراتب استثناها
یک برنامه واقعی یک خانواده از استثناها تعریف میکند تا بشود در سطوح مختلف آنها را گرفت.
ShopError <- the base: catch this to catch everything
+-- ProductError
| +-- ProductNotFound
| +-- OutOfStock
+-- PaymentError
+-- NotEnoughMoney
class ShopError(Exception):
'Base class of every error of the shop.'
class ProductError(ShopError):
'Something is wrong with a product.'
class ProductNotFound(ProductError):
def __init__(self, name: str) -> None:
super().__init__(f'The product {name!r} does not exist.')
self.name = name
class OutOfStock(ProductError):
def __init__(self, name: str, available: int) -> None:
super().__init__(f'Only {available} left of {name!r}.')
self.name = name
self.available = available
class PaymentError(ShopError):
'Something is wrong with the payment.'
class NotEnoughMoney(PaymentError):
def __init__(self, missing: float) -> None:
super().__init__(f'You need {missing:,.0f} more.')
self.missing = missing
خانواده استثناها: نمونه 11_exception_hierarchy.py
def buy(name: str, quantity: int, wallet: float) -> float:
if name not in STOCK:
raise ProductNotFound(name)
if STOCK[name] < quantity:
raise OutOfStock(name, STOCK[name])
total = PRICES[name] * quantity
if total > wallet:
raise NotEnoughMoney(total - wallet)
return wallet - total
def main() -> None:
for name, quantity, wallet in orders:
try:
rest = buy(name, quantity, wallet)
except ProductError as e:
# One handler for every product problem.
print(f'[product] {e}')
except PaymentError as e:
print(f'[payment] {e} (missing {e.missing:,.0f})')
except ShopError as e:
# The safety net for anything new added later.
print(f'[shop] {e}')
else:
print(f'Bought {quantity} x {name}, {rest:,.0f} left.')
گرفتن در سطوح مختلف: نمونه 11_exception_hierarchy.py
چرا این ساختار ارزش دارد
- صداکننده میتواند در سطح دلخواه خودش خطا را بگیرد: خیلی دقیق (
OutOfStock)، متوسط (ProductError)، یا کلی (ShopError). - اگر فردا خطای تازهای زیر
ProductErrorاضافه کنید، کدی کهProductErrorرا میگیرد بدون تغییر همچنان کار میکند. - گرفتن
ShopErrorدر بالاترین سطح یعنی «هر خطای شناختهشده این برنامه». هر چیز دیگری واقعاً یک باگ است و باید دیده شود. - ترتیب
exceptها از خاص به عام است. اگر برعکس بنویسید، اولی همه را میگیرد و بقیه بیفایده میشوند.
۱۲. زنجیره استثناها
# When you translate a low-level error into your own one, keep the original:
# 'raise ... from e' records the cause and shows both in the traceback.
class ConfigError(Exception):
'The configuration could not be read.'
def read_port(path: str) -> int:
try:
with open(path, encoding='utf-8') as f:
return int(f.read().strip())
except FileNotFoundError as e:
raise ConfigError(f'{path} is missing') from e
except ValueError as e:
raise ConfigError(f'{path} does not contain a number') from e
if __name__ == '__main__':
try:
read_port('nothing.cfg')
except ConfigError as e:
print('Error:', e)
print('Caused by:', repr(e.__cause__))
زنجیره: نمونه 12_raise_from.py
شرح مثال
- الگوی رایج: خطای سطح پایین (فایل نیست) را به خطای سطح بالای برنامهتان (تنظیمات خراب است) ترجمه میکنید.
from eعلت اصلی را نگه میدارد. در ردیابی هر دو دیده میشوند، با جمله «این خطا علت مستقیم خطای بعدی بود».- بدون
from، اطلاعات علت اصلی گم میشود و عیبیابی سختتر است. from Noneعمداً علت را پنهان میکند، وقتی که نمایش دادنش فقط کاربر را گیج میکند.
پوشه S24-oop-magic-methods-and-custom-exceptions شامل
نمونههای 01_str_and_repr.py تا
12_raise_from.py، بهعلاوه پوشه
09_custom_exception_package که یک استثنا را در ماژول
جداگانه نشان میدهد.
خلاصه فصل
| متد جادویی | وظیفه آن |
|---|---|
__str__ __repr__ | نمایش برای کاربر و برای برنامهنویس |
__len__ __bool__ __contains__ | رفتار مثل یک ظرف |
__eq__ __lt__ | مقایسه و مرتبسازی |
@total_ordering | ساختن بقیه عملگرهای مقایسه |
__hash__ | بعد از تعریف __eq__ لازم میشود |
__add__ __mul__ __neg__ | عملگرهای ریاضی. باید مقدار برگردانند |
__rmul__ | وقتی شیء شما سمت راست عملگر است |
__getitem__ __iter__ | اندیس و حلقه |
__call__ | شیئی که مثل تابع صدا زده میشود |
__enter__ __exit__ | ساختن مدیر زمینه برای with |
class MyError(Exception) | استثنای سفارشی |
| سلسلهمراتب استثنا | گرفتن خطا در سطح دلخواه |
raise X from e | حفظ علت اصلی خطا |
تمرینهای فصل بیست و چهار
- به کلاس
Bookخود،__str__و__repr__اضافه کنید. __eq__را طوری بنویسید که دو کتاب با شابک یکسان برابر باشند.__lt__را بنویسید تا لیستی از کتابها بر اساس سال مرتب شود.- کلاس
Fractionرا با__add__و__mul__و__str__بنویسید. - کلاس
Matrixرا با__getitem__و__add__بنویسید. - مدیر زمینه
OpenDatabaseرا بنویسید که «متصل شد» و «قطع شد» چاپ کند. - سلسلهمراتب استثنا برای یک برنامه کتابخانه طراحی کنید:
LibraryError،BookNotFound،AlreadyBorrowedو مانند این موارد. - یکی از برنامههای قبلیتان را طوری بازنویسی کنید که هر خطا به جای چاپ پیام، یکی از استثناهای خودتان را بیندازد.