Skip to content

Latest commit

 

History

History
525 lines (361 loc) · 29.6 KB

File metadata and controls

525 lines (361 loc) · 29.6 KB

Урок 10. Представление объектов: методы __str__ и __repr__


Зачем объекту нужно «лицо»

Когда вы создаёте экземпляр класса и пытаетесь вывести его на экран, 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__: представление для человека

Метод __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__: представление для разработчика

Метод __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-кодом — именно то, что нам нужно.


Разница между __str__ и __repr__ на практике

Чтобы разница стала совершенно очевидной, посмотрим на поведение полностью реализованного класса:

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__, он полезнее.


Практический пример: класс APIError

Перейдём к примеру, который напрямую отражает задачи веб-разработки. В любом веб-приложении существует понятие ошибки 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.


Более сложный пример: класс QueryResult

Рассмотрим ещё один практический пример — объект, который моделирует результат запроса к базе данных. Это поможет закрепить понимание того, как __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__, тогда как обратное неверно.


Вопросы

  1. Что выведет Python, если вы вызовете print() на объекте класса, в котором не определены ни __str__, ни __repr__? Является ли это ошибкой?
  2. В каких трёх ситуациях Python автоматически вызывает метод __str__?
  3. Чем отличается назначение __repr__ от назначения __str__?
  4. Что произойдёт, если метод __str__ вернёт не строку, а, например, целое число?
  5. У вас есть список объектов library = [book1, book2]. Какой из двух методов — __str__ или __repr__ — вызовет Python для каждого элемента при выполнении print(library)? Почему?
  6. Что означает суффикс !r в f-строке, например f"Book(title={self.title!r})"? Зачем он используется в реализации __repr__?
  7. Опишите поведение Python в двух ситуациях: когда в классе определён только __repr__ без __str__, и когда определён только __str__ без __repr__. Какой из двух методов важнее реализовать в первую очередь?
  8. В классе APIError из лекции метод __str__ возвращает краткое сообщение вида [404] Запрашиваемый ресурс не найден, тогда как __repr__ включает также путь запроса и HTTP-метод. Объясните, почему такое разделение оправдано с точки зрения архитектуры веб-приложения.
  9. Как ведут себя str() и repr() применительно к строке, содержащей спецсимволы, например s = "hello\nworld"? Почему их вывод различается?

Задачи

Задача 1.

Класс 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)

Задача 2.

Класс 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)

Задача 3.

Класс 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')]

Задача 4.

Класс 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

Задача 5

Класс 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...')

Задача 6.

Класс 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)

Предыдущий урок | Следующий урок