- Правильно реализовывать
__repr__,__str__,__eq__,__lt__,__hash__и методы операторов - Добавлять проверки через
@propertyи уместно использовать методы класса и статические методы - Объяснять MRO и работу
super()при множественном наследовании - Создавать абстрактный базовый класс с
abc.ABC
Почему + складывает числа, соединяет строки и склеивает списки? Почему len(), in, sorted() и print() работают с объектами совершенно разных типов? Потому что Python превращает каждую такую операцию в вызов специального метода: a + b становится a.__add__(b), а len(x) — x.__len__(). Их ещё называют dunder-методами (от англ. double underscore — двойное подчёркивание). Реализуй их в своём классе, и его объекты будут вести себя как встроенные типы. Этот набор правил называется моделью данных Python.
__repr__ и __str__: как объект представляется
Каждый класс наследует __repr__, который печатает что-то вроде <__main__.Money object at 0x7f…> — для отладки это бесполезно. __repr__ должен быть однозначным и адресован разработчикам; в идеале он выглядит как код, создающий объект. __str__ — читаемый вариант для пользователей. print() и f-строки используют __str__, а интерактивная консоль, списки и преобразование !r — __repr__. Если определён только __repr__, str() тоже использует его.
class Money:
def __init__(self, amount, currency='AZN'):
self.amount = amount
self.currency = currency
def __repr__(self):
return f'Money({self.amount!r}, {self.currency!r})'
def __str__(self):
return f'{self.amount:.2f} {self.currency}'
price = Money(12.5)
print(price)
print(repr(price))
print([price, Money(3, 'USD')])
print(f'{price} | {price!r}')▸ Ожидаемый результат
12.50 AZN Money(12.5, 'AZN') [Money(12.5, 'AZN'), Money(3, 'USD')] 12.50 AZN | Money(12.5, 'AZN')
Равенство, порядок и хеширование
Без __eq__ оператор == сравнивает идентичность: два объекта с одинаковыми данными не равны. Если тип другого операнда не поддерживается, __eq__ должен не возбуждать ошибку, а вернуть особое значение **NotImplemented: тогда Python попробует отражённую операцию у другого объекта, а если и она не сработает, сравнит идентичность. Декоратор functools.total_ordering** выводит <=, > и >= из __eq__ и __lt__:
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount, currency='AZN'):
self.amount, self.currency = amount, currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
def __lt__(self, other):
if not isinstance(other, Money) or other.currency != self.currency:
return NotImplemented
return self.amount < other.amount
def __hash__(self):
return hash((self.amount, self.currency))
prices = [Money(5), Money(2.5), Money(10)]
print([m.amount for m in sorted(prices)])
print(Money(5) == Money(5), Money(5) >= Money(2.5), Money(5) == 5)
print(len({Money(1), Money(1)}))▸ Ожидаемый результат
[2.5, 5, 10] True True False 1
Перегрузка операторов и протокол контейнера
Арифметика работает так же. Для a + b Python сначала вызывает a.__add__(b); если тот вернул NotImplemented, пробуется b.__radd__(a) — отражённый метод. Поэтому для 2 * v ниже нужен __rmul__: int ничего не знает о векторах. __bool__ определяет смысл if v:, а __abs__ обслуживает встроенную abs():
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f'Vector({self.x}, {self.y})'
def __add__(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 (self.x ** 2 + self.y ** 2) ** 0.5
def __bool__(self):
return bool(self.x or self.y)
v = Vector(3, 4)
print(v + Vector(1, 1), v * 2, 2 * v)
print(abs(v), bool(Vector(0, 0)))▸ Ожидаемый результат
Vector(4, 5) Vector(6, 8) Vector(6, 8) 5.0 False
| Запись | Вызываемый метод |
|---|---|
a + b, a - b, a * b | __add__, __sub__, __mul__ (__radd__, …) |
a == b, a < b | __eq__, __lt__ |
len(a), a[i], x in a | __len__, __getitem__, __contains__ |
for x in a | __iter__ (__getitem__) |
abs(a), bool(a), hash(a) | __abs__, __bool__, __hash__ |
str(a), repr(a), a() | __str__, __repr__, __call__ |
Для a += b Python сначала ищет __iadd__ (вариант, который может изменить объект на месте), а если его нет, использует __add__. Поэтому += меняет список на месте, а для кортежей и строк создаёт новый объект. Метод __call__ позволяет вызывать объект как функцию: obj().
Класс может вести себя и как коллекция. __len__ обслуживает len(), __getitem__ — индексы и срезы (объект среза просто передаётся списку), а __contains__ — оператор in. Работает даже for: если у класса нет __iter__, Python вызывает __getitem__ с 0, 1, 2…, пока не появится IndexError:
class Playlist:
def __init__(self, *songs):
self._songs = list(songs)
def __len__(self):
return len(self._songs)
def __getitem__(self, index):
return self._songs[index]
def __contains__(self, song):
return song.lower() in (s.lower() for s in self._songs)
p = Playlist('Morning', 'Rain', 'Finale')
print(len(p), p[0], p[-1], p[1:])
print('rain' in p)
for song in p:
print('>', song)▸ Ожидаемый результат
3 Morning Finale ['Rain', 'Finale'] True > Morning > Rain > Finale
@property, @classmethod и @staticmethod
Свойство (property) снаружи выглядит как обычный атрибут, но при чтении или присваивании выполняется метод. Это позволяет позже добавить проверку, не меняя код, который использует класс: t.celsius = -300 — по-прежнему обычное присваивание. Заметь, что __init__ тоже проходит через сеттер, поэтому даже новый объект не может начать жизнь в неверном состоянии. Свойство без сеттера, как fahrenheit, доступно только для чтения и вычисляется на лету:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius, t.fahrenheit)
try:
t.celsius = -300
except ValueError as e:
print('error:', e)▸ Ожидаемый результат
25 77.0 error: below absolute zero
Метод класса (@classmethod) получает не экземпляр, а сам класс в параметре cls. Главное его применение — альтернативные конструкторы вроде from_csv: поскольку метод вызывает cls(...), подкласс автоматически получает объекты своего типа. Статический метод (@staticmethod) не получает ни self, ни cls — это обычная функция, живущая в пространстве имён класса, потому что логически к нему относится. Обрати внимание и на атрибут класса count, общий для всех экземпляров:
class Student:
count = 0
def __init__(self, name, grade):
self.name, self.grade = name, grade
Student.count += 1
@classmethod
def from_csv(cls, line):
name, grade = line.split(',')
return cls(name, int(grade))
@staticmethod
def is_valid_grade(grade):
return 1 <= grade <= 11
def __repr__(self):
return f'{type(self).__name__}({self.name!r}, {self.grade})'
class Graduate(Student):
pass
print(Student.from_csv('Aysel,9'), Graduate.from_csv('Murad,11'))
print(Student.count, Student.is_valid_grade(12))▸ Ожидаемый результат
Student('Aysel', 9) Graduate('Murad', 11)
2 FalseНаследование, MRO и super()
При множественном наследовании Python нужно правило поиска методов. Он вычисляет MRO (method resolution order — порядок разрешения методов) алгоритмом C3: потомок идёт раньше родителей, родители сохраняют порядок, в котором перечислены, и каждый класс встречается один раз. Его видно в __mro__. Главное: **super() означает не «мой родитель», а «следующий класс в MRO настоящего объекта»**:
class Base:
def save(self):
print('Base.save')
class Logged(Base):
def save(self):
print('Logged: before')
super().save()
class Validated(Base):
def save(self):
print('Validated: checking')
super().save()
class Order(Logged, Validated):
pass
print([c.__name__ for c in Order.__mro__])
Order().save()▸ Ожидаемый результат
['Order', 'Logged', 'Validated', 'Base', 'object'] Logged: before Validated: checking Base.save
Logged напрямую наследуется от Base, но его super().save() вызвал Validated.save(), потому что именно он следующий в MRO класса Order. Благодаря этому Base.save() выполнился ровно один раз. Такие «сотрудничающие» классы, называемые миксинами (mixin), работают, только если каждый метод вызывает super() и ни один не вызывает родителя напрямую как Base.save(self).
Абстрактный базовый класс задаёт контракт, который обязаны выполнить подклассы. Экземпляр класса, унаследованного от abc.ABC и имеющего методы с пометкой @abstractmethod, нельзя создать, пока подкласс не реализует их все, — ошибка возникает при создании объекта, а не посреди работы:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): ...
def describe(self):
return f'{type(self).__name__} with area {self.area():.2f}'
class Rect(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
print(Rect(3, 4.5).describe())
try:
Shape()
except TypeError as e:
print(e)▸ Ожидаемый результат
Rect with area 13.50 Can't instantiate abstract class Shape without an implementation for abstract method 'area'
Добавь в класс Money методы __add__, __radd__ и __eq__, чтобы можно было складывать суммы, суммировать список через sum() и сравнивать результат. Для чужих типов __add__ должен возвращать NotImplemented.
class Money:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f'Money({self.amount})'
# add __add__, __radd__ and __eq__ here
wallet = [Money(5), Money(10), Money(2)]
print(Money(1) + Money(2))
print(sum(wallet))
print(sum(wallet) == Money(17))▸ Ожидаемый результат
Money(3) Money(17) True
Добавь в класс Stack методы __len__ и __repr__. Отдельный метод для bool(s) не пиши — пустой стек сам должен считаться False.
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
# add __len__ and __repr__ here
s = Stack()
print(bool(s))
s.push('a')
s.push('b')
print(s, len(s), bool(s))
print(s.pop(), s)▸ Ожидаемый результат
False Stack(['a', 'b']) 2 True b Stack(['a'])
Главное
- Операторы и встроенные функции вызывают специальные методы:
a + b→__add__,len(x)→__len__,x in c→__contains__. __repr__— для разработчиков,__str__— для пользователей; контейнеры показывают__repr__.__eq__для чужих типов должен возвращатьNotImplemented; определение__eq__убирает__hash__, если не добавить его явно.@propertyпрячет проверку за синтаксисом атрибута;@classmethodполучаетcls(альтернативные конструкторы),@staticmethod— ничего.super()следует MRO настоящего объекта; абстрактные методыABCдолжны быть реализованы до создания экземпляра.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
print([obj]), чтобы показать объект внутри списка?