Урок курса

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

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

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