برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۴

متدهای جادویی و استثناهای سفارشی

چرا len روی لیست کار می‌کند و روی کلاس شما نه؟ چرا می‌شود دو رشته را با + جمع کرد؟ در این فصل رازش را می‌فهمید و کلاس‌هایتان را مثل نوع‌های خود پایتون می‌کنید.

در این فصل می‌خوانید
  1. متد جادویی چیست
  2. __str__ و __repr__
  3. len و bool و in
  4. مقایسه و مرتب‌سازی
  5. سربارگذاری عملگرهای ریاضی
  6. اندیس و پیمایش
  7. شیء قابل صدا زدن و مدیر زمینه
  8. نمونه کامل: بردار
  9. اولین استثنای سفارشی
  10. استثنایی که داده حمل می‌کند
  11. سلسله‌مراتب استثناها
  12. زنجیره استثناها

۱. متد جادویی چیست

متدهایی که نامشان با دو زیرخط شروع و تمام می‌شود، متد جادویی یا 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

شرح مثال

۵. سربارگذاری عملگرهای ریاضی

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

شرح مثال

۸. نمونه کامل: بردار

'''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

چرا استثنای خودمان

۱۰. استثنایی که داده حمل می‌کند

# 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

چرا این ساختار ارزش دارد

۱۲. زنجیره استثناها

# 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

شرح مثال

فایل‌های تمرینی این فصل

پوشه 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حفظ علت اصلی خطا

تمرین‌های فصل بیست و چهار

  1. به کلاس Book خود، __str__ و __repr__ اضافه کنید.
  2. __eq__ را طوری بنویسید که دو کتاب با شابک یکسان برابر باشند.
  3. __lt__ را بنویسید تا لیستی از کتاب‌ها بر اساس سال مرتب شود.
  4. کلاس Fraction را با __add__ و __mul__ و __str__ بنویسید.
  5. کلاس Matrix را با __getitem__ و __add__ بنویسید.
  6. مدیر زمینه OpenDatabase را بنویسید که «متصل شد» و «قطع شد» چاپ کند.
  7. سلسله‌مراتب استثنا برای یک برنامه کتابخانه طراحی کنید: LibraryError، BookNotFound، AlreadyBorrowed و مانند این موارد.
  8. یکی از برنامه‌های قبلی‌تان را طوری بازنویسی کنید که هر خطا به جای چاپ پیام، یکی از استثناهای خودتان را بیندازد.
فصل قبل: وراثت و چندریختی فصل بعد: کد پایتونی، بخش اول