Урок курса

Специальные методы: __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]

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку