$ sudo teach IT
МОДУЛЬ 7 · УРОК 2

__str__ и __repr__ — два лица вашего объекта

Когда Python выводит объект на экран — он вызывает один из двух методов: __str__ или __repr__. Они делают похожую вещь, но для разных людей: __str__ — для пользователя, __repr__ — для разработчика. Разобравшись в разнице, вы избавитесь от загадочных строчек вроде <MyClass object at 0x7f3a...> раз и навсегда.

⏱ ~35 минут 🎓 Для новичков 🐍 Python 3

😕 Что происходит, если ничего не определять

Создадим простой класс и попробуем его напечатать. Многие новички сталкиваются с этим в первый же день:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


p = Point(3, 7)

print(p)
# <__main__.Point object at 0x10e4a2f50>

str(p)
# '<__main__.Point object at 0x10e4a2f50>'

🤔 Что это за строка? Здесь три части: __main__ — модуль, где определён класс; Point — имя класса; 0x10e4a2f50 — адрес объекта в памяти компьютера (в шестнадцатеричной системе). Эта информация полезна Python-интерпретатору, но абсолютно бесполезна нам. Мы не видим ни x = 3, ни y = 7.

Хорошая новость: это поведение по умолчанию, и его очень легко поменять — достаточно определить __str__ и/или __repr__.

👤 __str__ — читаемое представление для людей

Метод __str__ вызывается в трёх случаях: когда вы пишете print(obj), когда вы вызываете str(obj), и когда объект попадает в f-строку вроде f"Результат: {obj}". Смысл метода — вернуть человекочитаемую строку: красивую, понятную, удобную для отображения пользователю.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        # Этот метод должен вернуть строку (str)
        # Пишите то, что удобно ЧИТАТЬ
        return f"Точка({self.x}, {self.y})"


p = Point(3, 7)

print(p)
# Точка(3, 7)

str(p)
# 'Точка(3, 7)'

# В f-строке тоже работает __str__:
message = f"Координаты: {p}"
print(message)
# Координаты: Точка(3, 7)

✅ Правило __str__: возвращайте строку, которую не стыдно показать конечному пользователю. Это может быть «Корзина: 3 товара на сумму 1 290 ₽» или «Пользователь Иван Иванов (ivan@mail.ru)» — что-то информативное и читаемое.

Важно: метод __str__ обязан вернуть именно str (строку). Если вы вернёте, например, число — Python выбросит ошибку TypeError.

🔧 __repr__ — техническое представление для разработчика

Метод __repr__ (от английского representation — представление) создаёт строку для разработчика: для отладки, логирования, изучения состояния объекта. Он вызывается функцией repr(obj).

Есть золотое правило хорошего тона в Python:

🎯 Идеальный __repr__ возвращает строку, которую можно скопировать в Python и выполнить — и получить точно такой же объект. Другими словами: eval(repr(obj)) == obj. Шаблон: "ИмяКласса(param1=значение, param2=значение)".

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        # Строка должна показывать КАК воссоздать этот объект
        return f"Point(x={self.x}, y={self.y})"


p = Point(3, 7)

repr(p)
# 'Point(x=3, y=7)'

# В интерактивной консоли (REPL) объект выводится через __repr__:
# >>> p
# Point(x=3, y=7)

💡 Что такое REPL? REPL — это интерактивная консоль Python, та самая, где видите >>>. Когда вы просто вводите имя переменной и нажимаете Enter — Python показывает её через __repr__. Это сделано специально: разработчик, исследующий объект в консоли, хочет техническую информацию, а не «красивый» вывод.

⚖️ __str__ и __repr__ вместе — в чём разница

Посмотрим на реальный пример, где оба метода определены, и убедимся что они ведут себя по-разному:

class Product:
    def __init__(self, name, price, discount=0):
        self.name = name
        self.price = price
        self.discount = discount

    def __str__(self):
        # Для пользователя: красиво, понятно
        if self.discount:
            final = self.price * (1 - self.discount / 100)
            return f"{self.name} — {final:.0f} ₽ (скидка {self.discount}%)"
        return f"{self.name} — {self.price} ₽"

    def __repr__(self):
        # Для разработчика: технически точно, можно воссоздать объект
        return f"Product(name={self.name!r}, price={self.price}, discount={self.discount})"


p = Product("Курс Python", 1990, discount=20)

# print() вызывает __str__:
print(p)
# Курс Python — 1592 ₽ (скидка 20%)

# repr() вызывает __repr__:
print(repr(p))
# Product(name='Курс Python', price=1990, discount=20)

# str() вызывает __str__:
print(str(p))
# Курс Python — 1592 ₽ (скидка 20%)

# В REPL (просто ввести p и Enter) вызывается __repr__:
# >>> p
# Product(name='Курс Python', price=1990, discount=20)

👤

__str__ — для пользователя

  • Вызывается: print(obj), str(obj), f-строки
  • Цель: читаемость, красота
  • Пример: «Курс Python — 1592 ₽»

🔧

__repr__ — для разработчика

  • Вызывается: repr(obj), REPL (консоль)
  • Цель: точность, воспроизводимость
  • Пример: «Product(name='Курс Python', price=1990, discount=20)»

Обратите внимание на {self.name!r} в __repr__: суффикс !r в f-строке — это сокращение от repr(self.name). Он автоматически оборачивает строку в кавычки. Это удобно, чтобы сразу видеть тип значения (строка или число).

🔄 Если определён только __repr__ — str() тоже работает

Это важное правило Python: если __str__ не определён, Python использует __repr__ вместо него. То есть __repr__ — это запасной вариант для __str__.

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    # Только __repr__, __str__ НЕ определён
    def __repr__(self):
        return f"Temperature(celsius={self.celsius})"


t = Temperature(36.6)

# repr() — напрямую использует __repr__:
print(repr(t))
# Temperature(celsius=36.6)

# print() и str() — __str__ не найден → Python берёт __repr__:
print(t)
# Temperature(celsius=36.6)

print(str(t))
# Temperature(celsius=36.6)

💡 Практический вывод: если в вашем классе нет смысла разделять «красивый» и «технический» вывод — определяйте только __repr__. Этого достаточно, чтобы избавиться от <Temperature object at 0x...>. __str__ добавляйте только тогда, когда хотите разные виды вывода.

Обратная ситуация — только __str__ без __repr__ — не даёт такого удобства: repr(obj) всё равно вернёт некрасивый дефолтный вид. Поэтому если определяете один метод — это должен быть __repr__.

📋 В списках и контейнерах Python использует __repr__

Это тонкий момент, который многих удивляет. Когда вы помещаете объекты внутрь списка, словаря или другого контейнера и выводите весь контейнер — Python использует __repr__ для каждого элемента, а не __str__.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Точка({self.x}, {self.y})"

    def __repr__(self):
        return f"Point({self.x}, {self.y})"


p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = Point(5, 6)

# Выводим отдельный объект через print — работает __str__:
print(p1)
# Точка(1, 2)

# Помещаем объекты в список и выводим весь список:
points = [p1, p2, p3]
print(points)
# [Point(1, 2), Point(3, 4), Point(5, 6)]
# ^-- используется __repr__, а не __str__!

# То же самое в словаре:
d = {"a": p1, "b": p2}
print(d)
# {'a': Point(1, 2), 'b': Point(3, 4)}
# ^-- тоже __repr__

🧩 Почему так? Когда Python выводит контейнер (список, словарь, кортеж), он строит строку из всех элементов. Внутри контейнера элементы представляют через repr(), потому что это контекст «для разработчика» — вы изучаете структуру данных, вам нужна точная информация, а не пользовательский вид. Поэтому хороший __repr__ особенно важен для отладки.

Проверьте это сами: если убрать __repr__ из примера выше — список выведет [<__main__.Point object at 0x...>, ...]. Именно поэтому опытные Python-разработчики всегда определяют __repr__ в своих классах.

🏆 Пример идеального __repr__

Вот эталонный паттерн. Строка из __repr__ должна быть валидным Python-кодом, который можно выполнить и получить идентичный объект. Используйте шаблон ИмяКласса(param1=значение, param2=значение):

class BankAccount:
    def __init__(self, owner, balance, currency="RUB"):
        self.owner = owner
        self.balance = balance
        self.currency = currency

    def __str__(self):
        # Для пользователя: счёт Ивана, баланс 15 000 RUB
        return f"Счёт {self.owner}: {self.balance:,.0f} {self.currency}"

    def __repr__(self):
        # Для разработчика: как воссоздать этот объект
        return (
            f"BankAccount("
            f"owner={self.owner!r}, "
            f"balance={self.balance}, "
            f"currency={self.currency!r})"
        )


acc = BankAccount("Иван Петров", 15000, "RUB")

# Пользовательский вывод:
print(acc)
# Счёт Иван Петров: 15 000 RUB

# Технический вывод:
print(repr(acc))
# BankAccount(owner='Иван Петров', balance=15000, currency='RUB')

# Строку из repr() можно скопировать и выполнить:
# BankAccount(owner='Иван Петров', balance=15000, currency='RUB')
# → создаст точно такой же объект!

# В списке:
accounts = [acc, BankAccount("Анна Смирнова", 42000, "USD")]
print(accounts)
# [BankAccount(owner='Иван Петров', balance=15000, currency='RUB'),
#  BankAccount(owner='Анна Смирнова', balance=42000, currency='USD')]

✨ Суффикс !r: в f-строке {self.owner!r} эквивалентно {repr(self.owner)}. Для строки 'Иван Петров' это добавит кавычки: 'Иван Петров'. Используйте его для строковых атрибутов в __repr__ — тогда результат будет корректным Python-кодом.

📊 Сводная таблица: что вызывается когда

Запомните эту таблицу — она отвечает на 99% вопросов о __str__ и __repr__:

Ситуация Есть оба Только __repr__ Только __str__ Нет обоих
print(obj) __str__ __repr__ __str__ адрес в памяти
str(obj) __str__ __repr__ __str__ адрес в памяти
repr(obj) __repr__ __repr__ адрес в памяти адрес в памяти
REPL (просто obj) __repr__ __repr__ адрес в памяти адрес в памяти
[obj1, obj2] (в контейнере) __repr__ __repr__ адрес в памяти адрес в памяти

⚠️ Типичные ошибки новичков

❌ Ошибка 1: возвращать не строку

def __str__(self):
    return self.balance  # ❌ TypeError: balance — число, а не строка

def __str__(self):
    return str(self.balance)  # ✅ Правильно: явно конвертируем в строку

Оба метода __str__ и __repr__ обязаны возвращать объект типа str. Если вернёте число, список или None — получите TypeError.

❌ Ошибка 2: ждать __str__ в списке

# У класса есть __str__, но НЕТ __repr__
items = [obj1, obj2]
print(items)
# [<__main__.MyClass object at 0x...>, ...]  ← используется __repr__ (дефолтный)

# Решение: добавить __repr__

Помните: внутри контейнеров всегда используется __repr__. Не удивляйтесь, если красивый __str__ не помогает при выводе списка.

❌ Ошибка 3: вызывать dunder напрямую

print(obj.__str__())   # ❌ Некрасиво, хотя и работает
print(obj.__repr__())  # ❌ Некрасиво, хотя и работает

print(str(obj))        # ✅ Правильно
print(repr(obj))       # ✅ Правильно

Dunder-методы определяете вы, а вызывает Python через встроенные функции. Прямой вызов технически работает, но считается плохим стилем.

✅ Итоги урока

  • print(obj) и str(obj) вызывают __str__ — метод для пользователя: читаемый, красивый вывод.
  • repr(obj) и просто ввод объекта в REPL вызывают __repr__ — метод для разработчика: точная, диагностическая информация.
  • Если определён только __repr__ — он используется и вместо __str__. Обратное неверно.
  • Внутри контейнеров (список, словарь, кортеж) Python всегда использует __repr__ для элементов.
  • Идеальный __repr__: строка вида ИмяКласса(param1=значение, param2=значение), которую можно выполнить как Python-код.
  • Если определяете только один метод — пусть это будет __repr__. Он важнее для отладки.
  • Оба метода обязаны возвращать str. Возврат числа или None вызовет TypeError.

👉 В следующем уроке мы разберём __eq__ и операторы сравнения — как научить Python сравнивать ваши объекты с помощью ==, <, >. Вы увидите, почему без __eq__ два объекта с одинаковыми данными считаются «разными», и как это исправить.