Магические методы

⏱ 40 мин · 🧩 заданий: 5

Что такое магические методы

Магические (dunder — double underscore) методы — это методы с именами вида __имя__, которые Python вызывает неявно в ответ на синтаксис:

Код Что вызывается
len(x) x.__len__()
x + y x.__add__(y)
x[k] x.__getitem__(k)
x == y x.__eq__(y)
str(x), print(x) x.__str__()
for i in x x.__iter__()
x() x.__call__()
with x: x.__enter__() / x.__exit__()

Реализуя их, вы делаете свои объекты такими же удобными, как встроенные. Это и есть модель данных Python.

Представление

from datetime import date

class Money:
    def __init__(self, amount, currency="RUB"):
        self.amount, self.currency = amount, currency

    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"

    def __str__(self):
        return f"{self.amount:,.2f} {self.currency}".replace(",", " ")

    def __format__(self, spec):
        if spec == "short":
            return f"{self.amount:.0f}{self.currency[0]}"
        return str(self)

m = Money(1234567.5)
print(repr(m), str(m), f"{m}", f"{m:short}", sep="\n")

Арифметика

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):                 # m + other
        if isinstance(other, Money):
            return Money(self.amount + other.amount)
        if isinstance(other, (int, float)):
            return Money(self.amount + other)
        return NotImplemented                 # «не умею» — Python попробует other.__radd__

    def __radd__(self, other):                # other + m (если у other нет подходящего __add__)
        return self.__add__(other)

    def __mul__(self, k):
        return Money(self.amount * k)

    __rmul__ = __mul__

    def __neg__(self):
        return Money(-self.amount)

    def __repr__(self):
        return f"Money({self.amount})"

a, b = Money(100), Money(50)
print(a + b, a + 10, 10 + a, a * 3, 2 * a, -a)
print(sum([a, b, Money(1)]))                  # sum начинает с 0 → нужен __radd__

Отражённые и составные операторы

  • a + b: сначала a.__add__(b); если вернул NotImplemented — b.__radd__(a).
  • a += b: сначала a.__iadd__(b) (изменить на месте); если его нет — a = a + b.
Оператор Метод Отражённый На месте
+ __add__ __radd__ __iadd__
- __sub__ __rsub__ __isub__
* __mul__ __rmul__ __imul__
/ __truediv__ __rtruediv__ __itruediv__
//, %, ** __floordiv__, __mod__, __pow__ __r...__ __i...__
@ __matmul__ __rmatmul__ __imatmul__
&, \|, ^ __and__, __or__, __xor__ … …
унарные -x, +x, abs(x) __neg__, __pos__, __abs__

Контейнеры

class Playlist:
    def __init__(self, songs):
        self._songs = list(songs)

    def __len__(self):
        return len(self._songs)

    def __getitem__(self, index):            # p[i], p[a:b], и даже for и in!
        result = self._songs[index]
        return Playlist(result) if isinstance(index, slice) else result

    def __setitem__(self, index, value):
        self._songs[index] = value

    def __delitem__(self, index):
        del self._songs[index]

    def __contains__(self, song):            # song in p — быстрее, чем перебор через __getitem__
        return song in self._songs

    def __iter__(self):
        return iter(self._songs)

    def __repr__(self):
        return f"Playlist({self._songs})"

p = Playlist(["Intro", "Song A", "Song B", "Outro"])
print(len(p), p[0], p[1:3], "Song B" in p)
p[0] = "New Intro"
del p[-1]
for song in p:
    print("♪", song)
print(list(reversed(p)), sorted(p))

Даже одного __getitem__ достаточно, чтобы объект поддерживал for, in и reversed (вместе с __len__) — Python перебирает индексы 0, 1, 2… до IndexError.

Вызываемые объекты: call

class RateLimiter:
    def __init__(self, limit):
        self.limit = limit
        self.used = 0

    def __call__(self, request):
        if self.used >= self.limit:
            return f"429 Too Many Requests: {request}"
        self.used += 1
        return f"200 OK: {request}"

allow = RateLimiter(2)
for r in ["/a", "/b", "/c"]:
    print(allow(r))
print(callable(allow), callable(42))

Контекстный менеджер

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self                         # то, что попадёт в as

    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"прошло {self.elapsed * 1000:.1f} мс")
        return False                        # не подавлять исключения

with Timer() as t:
    sum(range(300_000))

Подробно — в уроке «Контекстные менеджеры».

Доступ к атрибутам

class Config:
    def __init__(self, data):
        self._data = data

    def __getattr__(self, name):              # только если атрибут не найден обычно
        try:
            return self._data[name]
        except KeyError:
            raise AttributeError(f"нет настройки {name!r}") from None

    def __setattr__(self, name, value):       # при ЛЮБОМ присваивании
        if not name.startswith("_") and not isinstance(value, (int, str, bool)):
            raise TypeError("настройки — только int, str, bool")
        super().__setattr__(name, value)      # обойти себя, чтобы не уйти в рекурсию

cfg = Config({"debug": True, "port": 8000})
print(cfg.debug, cfg.port)
cfg.timeout = 30
print(cfg.timeout)
try:
    cfg.missing
except AttributeError as e:
    print(e)
Метод Когда вызывается
__getattribute__(name) при каждом чтении атрибута
__getattr__(name) только если обычный поиск не нашёл
__setattr__(name, value) при каждом присваивании
__delattr__(name) при del obj.name

new и init

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            print("создаю объект")
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value):
        print("инициализирую", value)
        self.value = value

a = Singleton(1)
b = Singleton(2)              # __new__ вернул тот же объект, но __init__ вызвался снова!
print(a is b, a.value)

class UpperStr(str):          # наследование от неизменяемого типа — только через __new__
    def __new__(cls, value):
        return super().__new__(cls, value.upper())

print(UpperStr("привет"), type(UpperStr("x")))

Итог

  • Магические методы связывают объекты с синтаксисом: операторы, len, индексы, for, with, вызов.
  • Арифметика: __add__ → при NotImplemented __radd__; += — __iadd__ или a = a + b.
  • Контейнер: __len__, __getitem__ (+ __setitem__, __delitem__, __contains__, __iter__).
  • __call__ делает объект вызываемым; __enter__/__exit__ — контекстным менеджером.
  • __getattr__ — для отсутствующих атрибутов, __setattr__ — для всех присваиваний.
  • __new__ создаёт объект, __init__ его настраивает.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Создайте класс Vector(x, y) с операциями: - v1 + v2 и v1 - v2 — покоординатно; - v * k и k * v — умножение на число (нужен и __rmul__); - abs(v) — длина вектора; - v1 == v2 — сравнение координат; - repr(v) → Vector(1, 2).

Какие тесты проверяют код (6)
assert Vector(1, 2) + Vector(3, 4) == Vector(4, 6)
assert Vector(5, 5) - Vector(1, 2) == Vector(4, 3)
assert Vector(1, 2) * 3 == Vector(3, 6)
assert 3 * Vector(1, 2) == Vector(3, 6), "Нужен __rmul__"
assert abs(Vector(3, 4)) == 5
assert repr(Vector(1, 2)) == "Vector(1, 2)"
Задание 2 Программа на Python

Создайте класс Deck — колоду карт, которая ведёт себя как последовательность: len(deck), deck[0], срезы deck[:3], "A♠" in deck и for card in deck. Достаточно реализовать __len__ и __getitem__. Карты: ранги "2"…"10", "J", "Q", "K", "A" × масти "♠♥♦♣", порядок: сначала все масти для ранга 2, потом для 3 и т. д. Строка карты — ранг + масть, например "10♥".

Какие тесты проверяют код (5)
d = Deck(); assert len(d) == 52
d = Deck(); assert d[0] == "2♠" and d[-1] == "A♣"
d = Deck(); assert d[:3] == ["2♠", "2♥", "2♦"]
d = Deck(); assert "A♠" in d and "1♠" not in d
d = Deck(); assert sum(1 for _ in d) == 52
Задание 3 Программа на Python

Создайте класс Multiplier(k), экземпляр которого можно вызывать как функцию: triple = Multiplier(3); triple(5) → 15. Также посчитайте количество вызовов в атрибуте calls.

Какие тесты проверяют код (2)
t = Multiplier(3); assert t(5) == 15 and t(2) == 6 and t.calls == 2
assert callable(Multiplier(1))
Задание 4 Один вариант ответа

Чем __new__ отличается от __init__?

Задание 5 Один вариант ответа

Когда вызывается __getattr__?

Войдите, чтобы отмечать пройденные уроки.