Урок курса
Специальные методы: __repr__, __str__, __eq__, __len__
Python для продвинутых: ООП, типизация и тестированиеВ предыдущих уроках мы разбирались с тем, как устроены классы изнутри: атрибуты экземпляра, инкапсуляция, classmethod и staticmethod. Всё это — про то, как объект хранит данные и что с ними делает. Но есть ещё один пласт: как объект себя показывает. Python предоставляет несколько специальных методов, которые управляют представлением объекта в разных контекстах — в консоли, в логах, при сравнении, при подсчёте длины. В этом уроке разберём четыре из них: repr, str, eq и len.
repr и str: назначение и различие
Каждый объект в Python умеет превращаться в строку. Вопрос в том, для кого эта строка предназначена.
__repr__ — это представление для разработчика. Его задача — однозначно идентифицировать объект: показать, что это за тип и какие у него ключевые данные. Когда ты смотришь в отладчик, читаешь лог или просто вводишь объект в интерактивной сессии Python — ты видишь именно то, что возвращает __repr__.
__str__ — это представление для пользователя. Оно может быть более лаконичным, более «читабельным», без технических деталей. Именно его Python показывает при print() и в f-строках.
Если одним словом: __repr__ — для тебя как инженера, __str__ — для того, кто смотрит на вывод программы.
Распространённое заблуждение про __repr__
Многие считают, что __repr__ обязан возвращать строку, которую можно передать в eval() и получить обратно точно такой же объект. То есть что-то вроде:
"MyClass(name='Rock', count=3)"
Это полезная практика — но не обязательное требование. Стандарт Python говорит лишь о том, что строка должна быть информативной и однозначной. Если воспроизвести объект через eval() невозможно или слишком дорого — это нормально.
Чего точно не должен делать __repr__: возвращать пустую строку или строку без смысловой нагрузки. Дефолтный вывод вида <__main__.MyClass object at 0x7f3a2> — это и есть провал отсутствующего __repr__: технически работает, практически бесполезен.
При этом совпадение __repr__ и __str__ — вполне допустимая ситуация. Для простых типов (числовых обёрток, value objects) оба метода часто возвращают одно и то же. Разделять их стоит тогда, когда разным аудиториям нужны разные уровни детализации: например, разработчику важен полный список треков, а пользователю достаточно их количества.
Ключевая разница в одной фразе
__repr__ отвечает на вопрос «что это за объект?», __str__ — на вопрос «как это показать?». Иногда ответы совпадают, иногда нет. Именно поэтому в Python есть оба метода, а не один.
Правила диспетчеризации: когда Python вызывает repr и str
Разобравшись с тем, для кого предназначен каждый метод, разберём точные правила: в каком контексте Python выбирает тот или иной.
Таблица вызовов — print(obj) и str(obj) → вызывают __str__; если его нет — используется __repr__
repr(obj)→ вызывает только__repr__;__str__здесь не используетсяf'{obj}'→ вызывает__format__(obj, '')с пустой спецификацией формата; стандартная реализация__format__делегирует__str__, поэтому для большинства классов результат совпадает сstr(obj). Но если класс переопределяет__format__, поведение f-строки может отличаться отstr()— это не синонимы- Интерактивная сессия (вводишь
objи нажимаешь Enter) → вызывает__repr__ - Объект внутри контейнера (
[obj],{obj},(obj,)) → каждый элемент выводится через__repr__
Последний пункт часто удивляет. Если у тебя список объектов и ты делаешь print(my_list), Python выведет не __str__ каждого элемента, а именно __repr__. Это сделано намеренно: внутри контейнера важна диагностическая информация.
Отдельно про f-строки и format
Вот конкретный пример, где f'{obj}' и str(obj) дают разный результат:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __str__(self):
return f"{self.celsius}°C"
def __format__(self, spec):
if spec == 'f':
return f"{self.celsius:.1f}°C"
return f"[{self.celsius}°C]" # переопределяем даже пустую спецификацию
t = Temperature(20)
str(t) # '20°C' — вызывает __str__
f'{t}' # '[20°C]' — вызывает __format__ с пустой строкой
f'{t:f}' # '20.0°C' — вызывает __format__ с 'f'
Для учебного Playlist мы __format__ не переопределяем, поэтому f'{p}' и str(p) дадут одинаковый результат. Но на продвинутом уровне важно понимать, что это разные точки входа.
Что происходит, если одного метода нет
Если определён только __repr__, стандартные print() и str() используют его как запасной вариант; интерактивная сессия и контейнеры тоже показывают __repr__.
Если определён только __str__, print() и str() используют его, но repr(obj) и интерактивная сессия всё равно выдают дефолтный <...>-вывод.
class OnlyStr:
def __str__(self):
return "я понятен пользователю"
obj = OnlyStr()
print(obj) # я понятен пользователю
repr(obj) # '<__main__.OnlyStr object at 0x...>'
class OnlyRepr:
def __repr__(self):
return "OnlyRepr()"
obj = OnlyRepr()
print(obj) # OnlyRepr()
repr(obj) # 'OnlyRepr()'
Если реализуешь только один из двух методов, обычно полезнее начать с информативного __repr__.
eq и len: равенство по значению и поддержка len()
Если __repr__ и __str__ управляют тем, как объект выглядит, то __eq__ и __len__ управляют тем, как он себя ведёт — в сравнениях и при подсчёте размера.
Что происходит без eq
По умолчанию оператор == в Python сравнивает объекты по идентичности — то есть фактически проверяет, что это один и тот же объект в памяти, как is. Два объекта с абсолютно одинаковыми данными будут считаться неравными:
class Playlist:
def __init__(self, name, tracks):
self.name = name
self.tracks = tracks
p1 = Playlist("Rock", ["Song A", "Song B"])
p2 = Playlist("Rock", ["Song A", "Song B"])
print(p1 == p2) # False — разные объекты в памяти
print(p1 is p2) # False
Это типичная ловушка при написании тестов: ты создаёшь два объекта с одинаковыми полями, сравниваешь их через ==, получаешь False — и начинаешь искать баг там, где его нет.
Как работает eq
__eq__(self, other) вызывается каждый раз, когда Python видит ==. Твоя задача — определить, что именно означает «равенство» для твоего типа. Обычно это сравнение значимых атрибутов:
def __eq__(self, other):
if not isinstance(other, Playlist):
return NotImplemented
return self.name == other.name and self.tracks == other.tracks
Здесь важна строчка с NotImplemented. Если other — не Playlist, мы не возвращаем False, а возвращаем специальный синглтон NotImplemented. Это сигнал Python: «я не знаю, как сравнивать с этим типом — попробуй спросить у другой стороны». Python после этого вызовет other.__eq__(self), и если тот тоже вернёт NotImplemented — вот тогда уже будет False. Возврат False напрямую отрезает эту возможность и может сломать корректное сравнение с типом, который умеет сравниваться с Playlist.
len: размер объекта и булев контекст
__len__(self) возвращает неотрицательное целое число. Python вызывает его при len(obj). Для плейлиста естественный размер — количество треков:
def __len__(self):
return len(self.tracks)
Но у __len__ есть побочный эффект, о котором часто забывают: Python использует его при проверке истинности объекта в булевом контексте. Если __len__ возвращает 0, объект считается ложным:
empty = Playlist("Empty", [])
full = Playlist("Rock", ["Song A"])
if empty:
print("есть треки") # не выполнится
if full:
print("есть треки") # выполнится
bool(empty) # False
bool(full) # True
Это поведение работает автоматически — никакого __bool__ определять не нужно, пока есть __len__. Если хочешь переопределить булеву логику отдельно от размера, тогда уже нужен __bool__, но это отдельная история.
Одно жёсткое ограничение: __len__ обязан возвращать целое неотрицательное число. Попытка вернуть отрицательное значение или не-целое поднимет TypeError или ValueError на уровне интерпретатора — Python проверяет это за тебя.
Реализация всех четырёх методов в классе Playlist и проверка
Соберём всё вместе в одном классе. Playlist хранит имя плейлиста и список треков — достаточно, чтобы осмысленно реализовать каждый из четырёх методов.
class Playlist:
def __init__(self, name: str, tracks: list[str]):
self.name = name
self.tracks = tracks
def __repr__(self) -> str:
return f"Playlist(name={self.name!r}, tracks={self.tracks!r})"
def __str__(self) -> str:
return f"Playlist '{self.name}' ({len(self.tracks)} tracks)"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Playlist):
return NotImplemented
return self.name == other.name and self.tracks == other.tracks
def __len__(self) -> int:
return len(self.tracks)
В этом классе __repr__ и __eq__ используют одни и те же атрибуты (name и tracks), поэтому их поведение согласовано: объекты с одинаковыми данными имеют одинаковый repr и равны через ==. Это хорошая практика проектирования — держать repr и eq в согласии, — но Python не обеспечивает её автоматически. В другом классе одинаковый repr не обязан означать равенство, и равные объекты не обязаны иметь одинаковый repr. __str__ намеренно лаконичнее: показывает только количество треков, а не их содержимое. Порядок треков при сравнении важен, потому что self.tracks — список.
p1 = Playlist("Rock", ["Song A", "Song B"])
p2 = Playlist("Rock", ["Song A", "Song B"])
p3 = Playlist("Jazz", ["Blue Moon"])
empty = Playlist("Empty", [])
repr(p1)
# "Playlist(name='Rock', tracks=['Song A', 'Song B'])"
print(p1)
# Playlist 'Rock' (2 tracks)
f"{p1}"
# "Playlist 'Rock' (2 tracks)"
[p1, p3]
# [Playlist(name='Rock', tracks=['Song A', 'Song B']), Playlist(name='Jazz', tracks=['Blue Moon'])]
p1 == p2 # True
p1 == p3 # False
p1 == 42 # False — благодаря NotImplemented, который мы разобрали выше
p4 = Playlist("Rock", ["Song C", "Song D"])
p1 == p4 # False
repr(p4)
# "Playlist(name='Rock', tracks=['Song C', 'Song D'])"
len(p1) # 2
len(empty) # 0
bool(p1) # True
bool(empty) # False
Обратите внимание на пару p1 / p4: имя одинаковое, треки разные — __eq__ возвращает False, а __repr__ наглядно показывает разницу. Если бы __repr__ отображал только количество треков, оба плейлиста выглядели бы идентично в отладчике, хотя данные различаются. Именно поэтому здесь показываем полный список. Контейнер [p1, p3] использует __repr__ элементов — и благодаря этому в выводе видны реальные данные, а не лаконичные строки из __str__.
В следующем уроке @dataclass сгенерирует __repr__ и __eq__ автоматически; ручная реализация помогает понимать, что именно генерируется и почему.
Попробуйте решить
Дан класс:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point({self.x}, {self.y})'
def __str__(self):
return f'({self.x}, {self.y})'
p = Point(3, 4)
Что выведут/вернут следующие три выражения?
print(p)
repr(p)
[p]
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
