Когда вы создаёте экземпляр класса и пытаетесь вывести его на экран, Python должен как-то превратить объект в текст. Без какой-либо подсказки с вашей стороны интерпретатор делает это весьма формально: он сообщает тип объекта и его адрес в памяти.
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
book = Book("Мастер и Маргарита", "Булгаков", 1967)
print(book)Вывод будет примерно таким:
<__main__.Book object at 0x7f3a1c2b4e50>
Это не ошибка — Python честно говорит: «есть объект класса Book, и он находится по такому-то адресу в памяти». Но с точки зрения разработки такой вывод практически бесполезен. Когда вы отлаживаете программу, когда смотрите на список объектов в консоли, когда читаете сообщение об ошибке — вам нужна содержательная информация, а не адрес в памяти.
Именно эту проблему решают два магических метода: __str__ и __repr__. Они позволяют вам определить, как объект будет выглядеть в текстовом представлении. Но они решают эту задачу по-разному, и понимание разницы между ними — ключевой момент этого урока.
Метод __str__ отвечает за так называемое «читаемое» представление объекта. Его задача — вернуть строку, которая будет понятна конечному пользователю или разработчику, который быстро смотрит на вывод в консоли.
Именно этот метод вызывается в следующих ситуациях:
- при вызове встроенной функции
print(); - при явном приведении к строке через
str(); - при использовании объекта в f-строках и форматировании через
format().
Давайте добавим __str__ к нашему классу Book:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
# Возвращаем строку, удобную для чтения:
# автор — название (год)
return f'"{self.title}" — {self.author} ({self.year})'
book = Book("Мастер и Маргарита", "Булгаков", 1967)
print(book) # вызывает __str__
print(str(book)) # явное приведение, тоже вызывает __str__
print(f"Книга: {book}") # f-строка, тоже вызывает __str__Вывод:
"Мастер и Маргарита" — Булгаков (1967)
"Мастер и Маргарита" — Булгаков (1967)
Книга: "Мастер и Маргарита" — Булгаков (1967)
Теперь объект говорит о себе сам. Обратите внимание: метод __str__ обязан возвращать именно строку (str). Если вы вернёте что-то другое — например, число или None — Python выбросит исключение TypeError.
Метод __repr__ служит другой цели. Его название происходит от слова representation — «представление» в техническом смысле. Идеальный __repr__ должен возвращать строку, по которой можно однозначно воссоздать объект.
Классическое правило звучит так: если вы передадите результат repr(obj) в функцию eval(), вы должны получить объект, эквивалентный исходному.
Этот метод вызывается в следующих ситуациях:
- при вызове встроенной функции
repr(); - когда объект выводится в интерактивной консоли Python (REPL) без явного
print(); - когда объект находится внутри коллекции — списка, словаря, множества — и эта коллекция выводится через
print().
Последний пункт важно запомнить: если вы сделаете print([book1, book2]), Python вызовет __repr__ для каждого элемента списка, а не __str__.
Добавим __repr__ к нашему классу:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
return f'"{self.title}" — {self.author} ({self.year})'
def __repr__(self):
# Возвращаем строку, из которой можно воссоздать объект.
# Обратите внимание на экранирование кавычек внутри repr():
# строковые аргументы должны быть обёрнуты в кавычки явно.
return f"Book(title={self.title!r}, author={self.author!r}, year={self.year!r})"
book = Book("Мастер и Маргарита", "Булгаков", 1967)
print(repr(book)) # явный вызов __repr__
print([book]) # список вызывает __repr__ для элементовВывод:
Book(title='Мастер и Маргарита', author='Булгаков', year=1967)
[Book(title='Мастер и Маргарита', author='Булгаков', year=1967)]
Обратите внимание на конструкцию {self.title!r} в f-строке. Суффикс !r означает «применить repr() к этому значению перед подстановкой». Для строк это автоматически добавляет кавычки, что делает вывод __repr__ синтаксически корректным Python-кодом — именно то, что нам нужно.
Чтобы разница стала совершенно очевидной, посмотрим на поведение полностью реализованного класса:
book = Book("Мастер и Маргарита", "Булгаков", 1967)
# __str__ вызывается при выводе для пользователя
print(book)
# "Мастер и Маргарита" — Булгаков (1967)
# __repr__ вызывается при техническом представлении
print(repr(book))
# Book(title='Мастер и Маргарита', author='Булгаков', year=1967)
# В списке всегда используется __repr__
library = [
Book("Мастер и Маргарита", "Булгаков", 1967),
Book("1984", "Оруэлл", 1949),
]
print(library)
# [Book(title='Мастер и Маргарита', author='Булгаков', year=1967),
# Book(title='1984', author='Оруэлл', year=1949)]Правило, которое удобно держать в голове: __str__ — это то, что видит пользователь, __repr__ — это то, что видит разработчик при отладке.
Python имеет чёткую логику резервного поведения (fallback).
Если определён только __repr__, но не __str__, то Python будет использовать __repr__ во всех ситуациях — и при print(), и при repr(). Это делает __repr__ более «важным» из двух методов: если вы можете реализовать только один, реализуйте __repr__.
Если определён только __str__, но не __repr__, то repr() по-прежнему вернёт стандартное техническое представление вида <__main__.Book object at 0x...>.
Проверим это:
class OnlyStr:
def __str__(self):
return "Я умею только __str__"
class OnlyRepr:
def __repr__(self):
return "OnlyRepr()"
a = OnlyStr()
b = OnlyRepr()
print(str(a)) # "Я умею только __str__"
print(repr(a)) # <__main__.OnlyStr object at 0x...> — fallback
print(str(b)) # "OnlyRepr()" — __repr__ используется как fallback для __str__
print(repr(b)) # "OnlyRepr()"Практический вывод: всегда реализуйте оба метода. Но если проект небольшой и времени мало — начните с __repr__, он полезнее.
Перейдём к примеру, который напрямую отражает задачи веб-разработки. В любом веб-приложении существует понятие ошибки API — ситуации, когда сервер возвращает клиенту не данные, а описание проблемы. Типичный ответ об ошибке содержит код ошибки (числовой или строковой), человекочитаемое сообщение, и, возможно, дополнительные детали.
Рассмотрим, как __str__ и __repr__ решают две совершенно разные задачи в таком классе:
class APIError(Exception):
"""
Класс для представления ошибок API.
Наследуется от Exception, чтобы его можно было
использовать в конструкциях raise/try/except.
"""
def __init__(self, code, message, details=None):
self.code = code # числовой или строковой код ошибки
self.message = message # сообщение для пользователя
self.details = details # опциональные технические детали для лога
def __str__(self):
# Это представление предназначено для пользователя или клиента API.
# Оно должно быть понятным и не содержать технических подробностей.
return f"[{self.code}] {self.message}"
def __repr__(self):
# Это представление предназначено для разработчика и логов.
# Оно должно содержать всю информацию, необходимую для диагностики.
return (
f"APIError(code={self.code!r}, "
f"message={self.message!r}, "
f"details={self.details!r})"
)Теперь посмотрим, как этот класс ведёт себя в разных контекстах:
# Создаём объект ошибки
error = APIError(
code=404,
message="Запрашиваемый ресурс не найден",
details={"path": "/api/users/999", "method": "GET"}
)
# Вывод для пользователя — через print() или в ответе API
print(error)
# [404] Запрашиваемый ресурс не найден
# Вывод для лога — через repr()
print(repr(error))
# APIError(code=404, message='Запрашиваемый ресурс не найден',
# details={'path': '/api/users/999', 'method': 'GET'})
# Использование в исключении — try/except выводит через __str__
try:
raise error
except APIError as e:
print(f"Ошибка при обработке запроса: {e}")
# Ошибка при обработке запроса: [404] Запрашиваемый ресурс не найденОбратите внимание: когда исключение перехватывается через except и используется в f-строке, Python вызывает __str__. Пользователь видит краткое, понятное сообщение. При этом в системе логирования мы можем сохранить repr(e) — и тогда у нас будет полная картина произошедшего, включая путь запроса и метод HTTP.
Рассмотрим ещё один практический пример — объект, который моделирует результат запроса к базе данных. Это поможет закрепить понимание того, как __str__ и __repr__ могут передавать принципиально разный объём информации.
class QueryResult:
"""
Представляет результат выполнения SQL-запроса.
Хранит данные, метаданные о запросе и время выполнения.
"""
def __init__(self, query, rows, execution_time):
self.query = query # текст SQL-запроса
self.rows = rows # список строк результата (список словарей)
self.execution_time = execution_time # время выполнения в секундах
def __str__(self):
# Пользователю важно: сколько строк вернул запрос
# и как быстро это произошло. Текст запроса — лишний.
count = len(self.rows)
time_ms = round(self.execution_time * 1000, 2)
return f"QueryResult: {count} строк за {time_ms} мс"
def __repr__(self):
# Разработчику нужна полная картина: какой именно запрос выполнялся,
# сколько строк вернул и за какое время.
return (
f"QueryResult("
f"query={self.query!r}, "
f"rows={len(self.rows)} rows, "
f"execution_time={self.execution_time!r})"
)
# Имитируем результат запроса к базе данных
result = QueryResult(
query="SELECT * FROM users WHERE active = 1",
rows=[
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
],
execution_time=0.0423
)
print(result)
# QueryResult: 2 строк за 42.3 мс
print(repr(result))
# QueryResult(query='SELECT * FROM users WHERE active = 1',
# rows=2 rows, execution_time=0.0423)
# При хранении в списке — используется __repr__
results_log = [result]
print(results_log)
# [QueryResult(query='SELECT * FROM users WHERE active = 1',
# rows=2 rows, execution_time=0.0423)]Этот пример хорошо иллюстрирует философию двух методов. __str__ даёт оперативную сводку — всё, что нужно для быстрого понимания. __repr__ даёт полную техническую картину — всё, что нужно для диагностики и воспроизведения ситуации.
Стоит упомянуть, что встроенные типы Python сами реализуют оба метода, и это можно наблюдать непосредственно:
import datetime
d = datetime.date(2024, 6, 15)
print(str(d)) # 2024-06-15 — читаемая дата
print(repr(d)) # datetime.date(2024, 6, 15) — воссоздаваемое выражениеОбратите внимание: repr(d) возвращает строку datetime.date(2024, 6, 15), которую действительно можно передать в eval() и получить идентичный объект. Это и есть эталонная реализация __repr__.
Аналогично ведут себя числа, строки и другие встроенные типы:
x = 3.14
print(str(x)) # 3.14
print(repr(x)) # 3.14 — для простых чисел вывод совпадает
s = "hello\nworld"
print(str(s)) # hello
# world — строка выводится «как есть», перевод строки работает
print(repr(s)) # 'hello\nworld' — экранирует спецсимволы, добавляет кавычкиПоведение repr() для строк особенно показательно: он экранирует спецсимволы и оборачивает строку в кавычки. Это нужно именно потому, что repr() должен вернуть то, что можно скопировать и вставить в код Python как литерал.
Два метода — __str__ и __repr__ — решают одну задачу (текстовое представление объекта), но с разными аудиториями и разными целями.
__str__ — это публичное лицо объекта. Его реализуют так, чтобы вывод был понятен любому, кто читает результат работы программы. Он должен быть лаконичным, информативным и не перегруженным техническими деталями.
__repr__ — это техническое удостоверение объекта. Его реализуют так, чтобы разработчик мог по выводу однозначно понять, с каким именно объектом он имеет дело. В идеале — чтобы по этому выводу объект можно было воссоздать.
Если вы определяете только один метод — определяйте __repr__. Python использует его как резервное значение для __str__, тогда как обратное неверно.
- Что выведет Python, если вы вызовете
print()на объекте класса, в котором не определены ни__str__, ни__repr__? Является ли это ошибкой? - В каких трёх ситуациях Python автоматически вызывает метод
__str__? - Чем отличается назначение
__repr__от назначения__str__? - Что произойдёт, если метод
__str__вернёт не строку, а, например, целое число? - У вас есть список объектов
library = [book1, book2]. Какой из двух методов —__str__или__repr__— вызовет Python для каждого элемента при выполненииprint(library)? Почему? - Что означает суффикс
!rв f-строке, напримерf"Book(title={self.title!r})"? Зачем он используется в реализации__repr__? - Опишите поведение Python в двух ситуациях: когда в классе определён только
__repr__без__str__, и когда определён только__str__без__repr__. Какой из двух методов важнее реализовать в первую очередь? - В классе
APIErrorиз лекции метод__str__возвращает краткое сообщение вида[404] Запрашиваемый ресурс не найден, тогда как__repr__включает также путь запроса и HTTP-метод. Объясните, почему такое разделение оправдано с точки зрения архитектуры веб-приложения. - Как ведут себя
str()иrepr()применительно к строке, содержащей спецсимволы, напримерs = "hello\nworld"? Почему их вывод различается?
Класс Movie
Создайте класс Movie, который принимает три аргумента: title (название фильма, строка), director (режиссёр, строка) и year (год выхода, целое число).
Реализуйте метод __str__, который возвращает строку в формате "Название" (год), реж. Фамилия, и метод __repr__, который возвращает строку в формате Movie(title='...', director='...', year=...), пригодную для воссоздания объекта.
Оба метода должны использовать все три атрибута.
Пример использования:
film = Movie("Blade Runner 2049", "Вильнёв", 2017)
print(film) # "Blade Runner 2049" (2017), реж. Вильнёв
print(repr(film)) # Movie(title='Blade Runner 2049', director='Вильнёв', year=2017)Класс Coordinate
Создайте класс Coordinate, который принимает два аргумента: lat (широта, число с плавающей точкой) и lon (долгота, число с плавающей точкой).
Метод __str__ должен возвращать строку в формате 55.7558° N, 37.6173° E — то есть значения с четырьмя знаками после запятой и символом градуса.
Метод __repr__ должен возвращать строку в формате Coordinate(lat=55.7558, lon=37.6173).
Символ градуса в Python записывается как \u00b0 или непосредственно как °.
Пример использования:
point = Coordinate(55.7558, 37.6173)
print(point) # 55.7558° N, 37.6173° E
print(repr(point)) # Coordinate(lat=55.7558, lon=37.6173)Класс ServerLog
Создайте класс ServerLog, который принимает три аргумента: level (уровень лога, строка — например, "INFO", "WARNING", "ERROR"), message (текст сообщения, строка) и source (источник, строка — например, имя модуля или endpoint).
Метод __str__ должен возвращать строку в формате [LEVEL] сообщение — только уровень и текст, без указания источника, так как именно это отображается оператору в интерфейсе мониторинга.
Метод __repr__ должен возвращать полное техническое представление в формате ServerLog(level='...', message='...', source='...') — оно используется при записи в файл лога.
Пример использования:
entry = ServerLog("ERROR", "Соединение с базой данных прервано", "db/connection.py")
print(entry) # [ERROR] Соединение с базой данных прервано
print(repr(entry)) # ServerLog(level='ERROR', message='Соединение с базой данных прервано', source='db/connection.py')
logs = [entry]
print(logs) # [ServerLog(level='ERROR', message='Соединение с базой данных прервано', source='db/connection.py')]Класс Temperature
Создайте класс Temperature, который принимает один аргумент: celsius (температура в градусах Цельсия, число с плавающей точкой).
Класс должен иметь метод to_fahrenheit(), который вычисляет и возвращает температуру в градусах Фаренгейта по формуле F = C * 9/5 + 32.
Метод __str__ должен возвращать строку в формате 23.5°C / 74.3°F — то есть температуру сразу в обеих шкалах, каждое значение округлено до одного знака после запятой.
Метод __repr__ должен возвращать строку в формате Temperature(celsius=23.5).
Пример использования:
t = Temperature(23.5)
print(t) # 23.5°C / 74.3°F
print(repr(t)) # Temperature(celsius=23.5)
boiling = Temperature(100.0)
print(boiling) # 100.0°C / 212.0°FКласс UserSession
Создайте класс UserSession, который принимает три аргумента:
user_id(идентификатор пользователя, целое число),username(имя пользователя, строка)token(токен сессии, строка).
Токен — это конфиденциальные данные, поэтому в методе __str__ его отображать не нужно: верните строку в формате Session: username #user_id.
В методе __repr__, предназначенном для отладки, токен должен присутствовать, но отображаться в усечённом виде — только первые восемь символов, после которых следует ....
Формат __repr__: UserSession(user_id=..., username='...', token='12345678...').
Пример использования:
session = UserSession(42, "alice", "a1b2c3d4e5f6g7h8i9j0")
print(session) # Session: alice #42
print(repr(session)) # UserSession(user_id=42, username='alice', token='a1b2c3d4...')Класс HttpRequest
Создайте класс HttpRequest, который принимает четыре аргумента:
method(HTTP-метод, строка — например,"GET","POST"),path(путь запроса, строка — например,"/api/users/5"),body(тело запроса, словарь, по умолчаниюNone)headers(словарь заголовков, по умолчаниюNone).
Метод __str__ должен возвращать строку в формате GET /api/users/5 — только метод и путь, без тела и заголовков, поскольку именно такой формат используется в HTTP-протоколе для краткого обозначения запроса.
Метод __repr__ должен возвращать полное представление в формате HttpRequest(method='GET', path='/api/users/5', body=None, headers=None), включая все четыре атрибута, независимо от того, переданы ли body и headers или остаются None.
Пример использования:
req = HttpRequest(
method="POST",
path="/api/users",
body={"name": "Bob", "email": "bob@example.com"},
headers={"Content-Type": "application/json"}
)
print(req) # POST /api/users
print(repr(req)) # HttpRequest(method='POST', path='/api/users',
# body={'name': 'Bob', 'email': 'bob@example.com'},
# headers={'Content-Type': 'application/json'})
empty_req = HttpRequest("GET", "/api/health")
print(empty_req) # GET /api/health
print(repr(empty_req)) # HttpRequest(method='GET', path='/api/health', body=None, headers=None)