Предскажите вывод программы и назовите протокол, который определяет поведение f"{x}". пример с кодом

Предскажите вывод программы и назовите протокол, который определяет поведение f"{x}".

class Item:
    def __str__(self):
        return "STR"

    def __repr__(self):
        return "REPR"

x = Item()
print(f"{x}")
print(f"{x!r}")
print(format(x, ""))
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Программа напечатает:

STR REPR STR

f"{x}" и format(x, "") используют протокол форматирования через __format__. У обычного объекта реализация object.__format__ при пустой спецификации обращается к __str__. Флаг !r сначала явно выполняет repr(x), поэтому во второй строке вызывается __repr__.

Исторический контекст

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

Протокол __format__ появился как общий механизм форматирования, используемый функцией format() и строковыми шаблонами. Он позволяет классу поддерживать собственные спецификаторы, например формат дат, чисел или денежных величин, не смешивая их с логикой обычного преобразования в строку.

Постановка проблемы

Неверное понимание этих протоколов приводит к неожиданному выводу в f-строках, логах и сообщениях об ошибках. Особенно важно отличать обычное форматирование от явного преобразования !r, !s или !a.

Если класс реализует только __str__, это не означает, что любая форма форматирования будет работать: для непустой спецификации может потребоваться собственный __format__. Поэтому выбор метода зависит не только от самого объекта, но и от синтаксиса вызова.

Подробное решение

Для выражения f"{x}" Python фактически использует форматирование объекта с пустой спецификацией: format(x, ""). Поиск специального метода выполняется на уровне типа объекта, а не через обычный поиск атрибута в экземпляре.

У Item нет собственного __format__, поэтому используется реализация object.__format__. При пустой спецификации она возвращает результат str(x), то есть вызывает Item.__str__, и получается STR.

В выражении f"{x!r}" флаг !r сначала применяет repr(x). Результатом становится строка REPR; затем эта строка форматируется с пустой спецификацией. Поэтому Item.__format__ здесь не вызывается.

Вызов format(x, "") снова использует object.__format__ с пустой спецификацией и приводит объект через __str__. Если бы в классе был определён собственный __format__, он получил бы строку спецификации и мог бы выбрать другое представление.

Флаги преобразования работают следующим образом:

  • !s сначала вызывает str(x);
  • !r сначала вызывает repr(x);
  • !a сначала вызывает ascii(x);
  • после преобразования к полученному значению применяется форматирование.

Собственный __format__ обычно нужен, когда класс поддерживает осмысленные непустые спецификации. Например, объект цены может обрабатывать ".2f", а дата — собственные варианты представления. Компромисс состоит в том, что такой интерфейс нужно документировать и согласовывать с ожиданиями стандартного форматирования.

Ситуация из практики

В сервисе есть объект денежной суммы. Разработчик переопределил только __str__, а затем написал f"{amount:.2f}". Вызов завершается TypeError, потому что у object.__format__ нет общего поведения для непустой спецификации.

Возможны три подхода:

  • форматировать внутреннее число снаружи: f"{amount.value:.2f}"; это просто, но детали устройства класса распространяются по коду;
  • реализовать __format__; это даёт естественный интерфейс f"{amount:.2f}", но требует определить допустимые спецификации;
  • использовать только __str__; это подходит для отображения, но не решает задачу точного форматирования.

Обычно выбирают собственный __format__, если объект регулярно форматируется в разных местах. При этом __str__ оставляют для человекочитаемого значения, а __repr__ делают информативным для диагностики. Такой контракт уменьшает дублирование и не заставляет вызывающий код обращаться к внутренним полям.

Что кандидаты часто упускают

  1. Что произойдёт при format(x, ".2f"), если у класса нет собственного __format__?

    Для обычного объекта object.__format__ не умеет интерпретировать произвольную непустую спецификацию и обычно возбуждает TypeError. Наличие __str__ или __repr__ само по себе не добавляет поддержку числовых форматов.

  2. Вызывается ли собственный __format__ объекта при использовании !r?

    Преобразование выполняется первым. !r получает строку от repr(x), после чего форматируется уже эта строка, обычно с помощью str.__format__. Поэтому Item.__format__ не управляет исходным объектом в выражении f"{x!r}".

  3. Обязан ли __repr__ возвращать строку, которую можно выполнить для восстановления объекта?

    Нет, это полезная, но не обязательная договорённость. Метод обязан вернуть объект типа str, иначе Python возбуждает TypeError. Представление, пригодное для диагностики или воспроизведения объекта, желательно, но язык не гарантирует и не требует его исполнимости.