Dunder-методы: repr, eq, hash, len, iter
Dunder-методы (double underscore - __method__) это специальные методы, которые Python вызывает автоматически в определённых контекстах. Они позволяют твоему классу интегрироваться с языком: работать с операторами, встроенными функциями, циклами и многим другим. В этом уроке - самые важные dunder-методы.
Общая идея
class Money:
def __init__(self, amount):
self.amount = amount
m1 = Money(100)
m2 = Money(50)
m1 + m2 # TypeError - + не определён для Money
# Добавляем __add__
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
m1 + m2 # Money(150)
+ вызывает __add__. Аналогично == вызывает __eq__, len() вызывает __len__, for x in obj использует __iter__. Реализуя dunder-методы, ты «учишь» класс работать со стандартными конструкциями Python.
repr и str
__repr__ - официальное представление, для отладки:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
u = User("Alice", 30)
print(repr(u)) # User(name='Alice', age=30)
print(u) # User(name='Alice', age=30) - использует __repr__ если __str__ нет
__str__ - человекочитаемое представление, для конечного пользователя:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
def __str__(self):
return f"{self.name} ({self.age})"
u = User("Alice", 30)
print(str(u)) # Alice (30)
print(repr(u)) # User(name='Alice', age=30)
print(u) # Alice (30) - print использует __str__
Конвенция: __repr__ должен (по возможности) возвращать строку которая в eval() даст эквивалентный объект. __str__ для humans.
Минимум: всегда определяй __repr__, __str__ опционально.
eq и hash
По умолчанию == сравнивает по identity (адресу в памяти):
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
Point(1, 2) == Point(1, 2) # False - разные объекты
С __eq__ - по значению:
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
Point(1, 2) == Point(1, 2) # True
Важно: определяя __eq__, ты автоматически отключаешь __hash__ - объект становится unhashable:
{Point(1, 2)} # TypeError - unhashable
Чтобы вернуть hashable, реализуй __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))
{Point(1, 2), Point(1, 2), Point(3, 4)} # {Point(1,2), Point(3,4)}
Обратное не обязательно: hash(a) == hash(b) не значит a == b (могут быть hash collisions).
Если меняется ключевое для eq поле, нужно либо избегать использования объекта в set и dict, либо делать класс immutable.
lt, le, gt, ge - сравнение
Для сортировки:
class Version:
def __init__(self, major, minor, patch):
self.major, self.minor, self.patch = major, minor, patch
def _key(self):
return (self.major, self.minor, self.patch)
def __lt__(self, other):
return self._key() < other._key()
def __eq__(self, other):
return self._key() == other._key()
def __repr__(self):
return f"v{self.major}.{self.minor}.{self.patch}"
versions = [Version(1, 2, 3), Version(2, 0, 0), Version(1, 10, 0)]
print(sorted(versions)) # [v1.2.3, v1.10.0, v2.0.0]
sorted и min/max используют __lt__. Для полного набора операторов используется @functools.total_ordering:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major, self.minor, self.patch = major, minor, patch
def __eq__(self, other):
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other):
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
# total_ordering генерирует __le__, __gt__, __ge__, __ne__ из __eq__ и __lt__
len
Поддержка len():
class Inbox:
def __init__(self):
self.messages = []
def __len__(self):
return len(self.messages)
inbox = Inbox()
inbox.messages = ["msg1", "msg2", "msg3"]
print(len(inbox)) # 3
__len__ также делает объект truthy/falsy: пустой считается False:
if not inbox:
print("Пусто")
Если хочется явно управлять truthiness без __len__, используется __bool__:
class Connection:
def __init__(self):
self.is_open = False
def __bool__(self):
return self.is_open
getitem, setitem, delitem, contains
Доступ как к коллекции:
class WordList:
def __init__(self, words):
self.words = words
def __getitem__(self, index):
return self.words[index]
def __setitem__(self, index, value):
self.words[index] = value
def __delitem__(self, index):
del self.words[index]
def __contains__(self, value):
return value in self.words
def __len__(self):
return len(self.words)
wl = WordList(["apple", "banana", "cherry"])
print(wl[0]) # apple - __getitem__
wl[0] = "avocado" # __setitem__
del wl[1] # __delitem__
print("cherry" in wl) # True - __contains__
print(len(wl)) # 2
С __getitem__ и __len__ объект становится sequence-подобным. Можно делать for item in wl - даже без __iter__.
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
result = self.current
self.current -= 1
return result
for x in Countdown(3):
print(x) # 3, 2, 1
__iter__ возвращает iterator (часто self). __next__ возвращает следующее значение или бросает StopIteration для остановки.
Часто проще через генератор:
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
for x in Countdown(3):
print(x)
Генераторы создают итератор автоматически. Подробно - в уроках про итераторы и генераторы.
call - объект как функция
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
print(double(5)) # 10
print(double(7)) # 14
После __call__ инстанс ведёт себя как функция. Полезно для:
- Функций с состоянием
- Stateful callbacks
- Декораторов через классы
enter и exit - context manager
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"Прошло: {self.elapsed:.4f}s")
with Timer() as t:
sum(x ** 2 for x in range(1_000_000))
# Прошло: 0.05s
Подробно про context managers - в уроке 16.
Арифметические операторы
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, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # scalar * vector
return self.__mul__(scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) - через __rmul__
Полный список арифметических dunder:
| Оператор | Метод |
|---|---|
+ | __add__, __radd__ |
- | __sub__, __rsub__ |
* | __mul__, __rmul__ |
/ | __truediv__, __rtruediv__ |
// | __floordiv__ |
% | __mod__ |
** | __pow__ |
+= | __iadd__ (или fallback на __add__) |
__r<op>__ (reflected) вызывается когда левый операнд не реализует операцию. __i<op>__ (inplace) для +=, -= и т.д.
NotImplemented
Когда операция не определена для конкретных типов:
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.currency != other.currency:
return NotImplemented
return Money(self.amount + other.amount, self.currency)
Возврат NotImplemented (не NotImplementedError) говорит Python попробовать reflected-метод другого операнда. Если оба вернули - получишь TypeError.
Распространённые ошибки
1. eq без hash
class User:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
users = {User("Alice"), User("Bob")} # TypeError - unhashable
Реши - либо тоже определи __hash__, либо явно __hash__ = None (явная неhashability).
2. Mutable объект как ключ dict
class MutableUser:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
u = MutableUser("Alice")
d = {u: "value"}
u.name = "Bob" # ОПАСНО - hash изменился!
print(u in d) # False - ключ "потерян" в hash-таблице
Hashable объекты должны быть immutable (или хотя бы не меняться по hash-зависимым полям).
3. init возвращает значение
class A:
def __init__(self):
return 5 # TypeError: __init__() should return None
__init__ всегда возвращает None.
Полный список dunder-методов
Часто используемые:
| Метод | Контекст |
|---|---|
__init__ | конструктор |
__repr__ | repr(), отладка |
__str__ | str(), print() |
__eq__, __hash__ | сравнение и hash |
__lt__, __le__, __gt__, __ge__ | сравнение |
__len__ | len(), truthiness |
__bool__ | bool() |
__getitem__, __setitem__, __delitem__ | индексация |
__iter__, __next__ | итерация |
__contains__ | оператор in |
__call__ | вызов как функция |
__enter__, __exit__ | context manager |
__add__, __sub__, __mul__ etc | арифметика |
__getattr__, __setattr__ | доступ к атрибутам |
Менее частые: __new__, __del__, __copy__, __deepcopy__, __reduce__ (pickle).
Мини-задание
- Класс Money с операторами:
class Money:
def __init__(self, amount, currency="RUB"):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount}, {self.currency!r})"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __hash__(self):
return hash((self.amount, self.currency))
def __add__(self, other):
if not isinstance(other, Money) or self.currency != other.currency:
return NotImplemented
return Money(self.amount + other.amount, self.currency)
a = Money(100)
b = Money(50)
c = Money(100, "USD")
print(a + b) # Money(150, 'RUB')
print(a == Money(100)) # True
# print(a + c) # TypeError - разные валюты
- Итерируемая коллекция:
class FibonacciSequence:
def __init__(self, count):
self.count = count
def __iter__(self):
a, b = 0, 1
for _ in range(self.count):
yield a
a, b = b, a + b
for n in FibonacciSequence(10):
print(n, end=" ")
# 0 1 1 2 3 5 8 13 21 34
- Объект как функция:
class Pipeline:
def __init__(self, *functions):
self.functions = functions
def __call__(self, value):
for fn in self.functions:
value = fn(value)
return value
pipeline = Pipeline(
lambda x: x.strip(),
lambda x: x.lower(),
lambda x: x.replace(" ", "-"),
)
print(pipeline(" Hello World ")) # hello-world
Что дальше
Освоили dunder-методы - класс теперь интегрирован с языком. В следующем уроке - наследование и MRO: как один класс наследует другой, что такое multiple inheritance и алгоритм C3 для разрешения порядка методов.