Урок курса
Специальные методы: __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__ автоматически; ручная реализация помогает понимать, что именно генерируется и почему.
Задача 1
Дан класс:
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]
Задача 2
Разработчик написал класс, где определён только __repr__, но не __str__:
class Tag:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Tag(name={self.name!r})'
t = Tag('python')
print(t)
Что выведет print(t)?
Задача 3
Дан класс без __eq__:
class Invoice:
def __init__(self, number, amount):
self.number = number
self.amount = amount
a = Invoice('INV-001', 500)
b = Invoice('INV-001', 500)
print(a == b)
Что выведет print(a == b) и почему?
Задача 4
Разработчик написал __repr__ для класса:
class Server:
def __init__(self, host, port, is_active):
self.host = host
self.port = port
self.is_active = is_active
def __repr__(self):
return f'connected to {self.host}'
Коллега говорит, что __repr__ написан плохо. Какое замечание точнее всего описывает проблему?
Задача 5
Дан класс:
class Queue:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def __len__(self):
return len(self._items)
q = Queue()
if not q:
print('пусто')
q.push('task1')
print(len(q))
Что выведет этот код?
Задача 6
Разработчик написал __eq__ следующим образом:
class Product:
def __init__(self, sku, price):
self.sku = sku
self.price = price
def __eq__(self, other):
return self.sku == other.sku and self.price == other.price
p = Product('A1', 100)
print(p == 'not a product')
Что произойдёт при выполнении print(p == 'not a product') и в чём проблема с этой реализацией?
Задача 7
Класс Book с repr, str, eq и len
Реализуйте класс Book, представляющий книгу со списком глав.
Что уже дано
Проверяющая система не создаёт заранее никаких переменных или файлов. После выполнения вашего кода скрытый проверщик сам создаёт объекты Book и проверяет их поведение. Видимый каркас класса — отправная точка: дополните его четырьмя специальными методами.
Что нужно сделать
Допишите класс Book со следующим поведением:
__init__(self, title: str, author: str, chapters: list[str])— сохраняет три атрибута экземпляра:title,author,chapters.__repr__(self) -> str— возвращает строку видаBook(title='Дюна', author='Херберт', chapters=3), гдеchapters— количество глав (целое число). Строка должна однозначно идентифицировать объект для разработчика.__str__(self) -> str— возвращает строку вида«Дюна» — Херберт (3 гл.). Формат:«{title}» — {author} ({n} гл.), гдеn— количество глав.__eq__(self, other) -> bool— два объектаBookравны, если совпадаютtitle,authorиchapters. Для объектов другого типа возвращайтеNotImplemented.__len__(self) -> int— возвращает количество глав.
Ввод и вывод
stdin не используется. Печатать через print в stdout ничего не нужно.
Проверяющая система создаёт объекты Book напрямую в коде и проверяет возвращаемые значения repr(), str(), == и len(). Никакого ввода от пользователя нет.
Пример использования
book = Book("Python без магии", "А. Автор", ["Старт", "Классы"])
print(str(book))
print(repr(book))
print(len(book))
Результат:
«Python без магии» — А. Автор (2 гл.)
Book(title='Python без магии', author='А. Автор', chapters=2)
2
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- Специальные методы: repr, str, eq, len — задача по Python
Практическая задача по теме «Специальные методы: repr, str, eq, len» с проверкой ответа онлайн.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
