Магические методы
Что такое магические методы
Магические (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
Создайте класс 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)"
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, k):
return Vector(self.x * k, self.y * k)
__rmul__ = __mul__
def __abs__(self):
return math.hypot(self.x, self.y)
def __eq__(self, other):
return isinstance(other, Vector) and (self.x, self.y) == (other.x, other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
Создайте класс 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
class Deck:
ranks = [str(n) for n in range(2, 11)] + list("JQKA")
suits = "♠♥♦♣"
def __init__(self):
self._cards = [r + s for r in self.ranks for s in self.suits]
def __len__(self):
return len(self._cards)
def __getitem__(self, index):
return self._cards[index]
Создайте класс 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))
class Multiplier:
def __init__(self, k):
self.k = k
self.calls = 0
def __call__(self, x):
self.calls += 1
return x * self.k
Чем __new__ отличается от __init__?
Когда вызывается __getattr__?
Войдите, чтобы отмечать пройденные уроки.