Форматирование строк

В прошлом уроке мы научились выводить текст, но не уделили внимания выводу переменных. В простых случаях можно просто вывести переменную или воспользоваться объединением строк. Но уже если с помощью + попытаться сложить число и текст, то будет ошибка. Поэтому необходимо конвертировать тип переменной, что делает код совсем некрасивым:

>>> spam = 10
>>> print("Spam = "+spam)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> print("Spam = "+str(spam))
Spam = 10

Сразу хочется вспомнить первую строку Дзена Питона "Красивое лучше, чем уродливое" и признать, что вывод строки через объединение строк с преобразованием типов выглядит не так красиво. Это заметили не только вы, но и авторы языка и постепенно сделали вывод текста красивым. Давайте посмотрим на эволюцию способов форматирования строк:

>>> item = "часы"
>>> minutes = 25
>>> # Самый старый способ через оператор %
>>> "Внимание %s, отстают на %d минут" % (item, minutes)
'Внимание часы, отстают на 25 минут'
>>> # Новый стиль через форматирование
>>> "Внимание {}, отстают на {} минут".format(item, minutes)
'Внимание часы, отстают на 25 минут'
>>> # Интерполяция строк (интерполяция — более поздняя вставка строк)
>>> f"Внимание {item}, отстают на {minutes} минут"
'Внимание часы, отстают на 25 минут'
>>> # Работа с шаблонами
>>> from string import Template
>>> t = Template('Привет, $name!')
>>> type(t)
<class 'string.Template'>
>>> t.substitute(name="Марти")
'Привет, Марти!'

После такого разнообразия хочется тут же процитировать еще одну строку Дзена Питона "Должен существовать один — и, желательно, только один — очевидный способ сделать это". К сожалению, особенность развития технологий такая, что после того как вы уже сделали что-то, то потом сложно это убрать. На Python написано столько приложений, что даже после того как к старому добавились новые способы форматирования строк старый уже никак не убрать из языка. Но пусть вас не пугает то, что в языке есть несколько способов получить один и тот же результат. Работа с форматированим строк — это наглядный пример того, что язык развивается и комитет разработчиков с трепетом следит за удобством синтаксиса. Кроме того все эти способы не заменяют, а дополняют друг-друга и то что можно сделать одним способом не всегда возможно сделать другим.

"Старый стиль" форматирования строки через оператор %

Исторически стиль форматирования строк берет начало от printf-стиля в языке программирования Си. Этот стиль позволяет в строке указать специальные места в которые потом будут подставлены значения. Причем важно следить чтобы количество параметров было достаточно чтобы успешно обработать строку, иначе будет выдано сообщение об ошибке. Места для переменных указываются следующим образом:

  1. Символ % указывает начало места для подстановки.
  2. Опционально может быть указано имя ключа в (скобках), например (name).
  3. Опции форматирования и количества знаков после запятой, более подробно лучше смотреть в документации.
  4. Тип переменной.

Пример: "Привет %(name)s", тут начало подстановки %, потом имя переменной name в скобках и завершает тип переменной s.

Краткий список типов переменных:

  • d, i — целые числа со знаком.
  • e, E — число с плавающим знаком в экспоненциальной форме в нижнем и верхнем регистре.
  • f, F — число с плавающим знаком, разные варианты влияют на то будет ли показываться десятичная часть, по умолчанию 6 знаков после запятой.
  • o, x, X — вывод числа в восьмеричной, шестнадцатеричной и шестнадцатеричной форме в верхнем регистре.
  • c — символ, в Си для строк длинной в 1 символ есть отдельный тип.
  • r, s, a — отображение строки эквивалентное вызову функций repr, str, ascii.
  • % — для случаев когда надо вывести знак процентов, то есть просто дважды записать %%.

Когда надо вывести строку с одной переменной, то используется запись str % val. Если несколько позиционных, то str % (val, val2). Если используются именованные ключи, то они передаются в формате словаря str % {"key": value, "another": value2}:

>>> name = "Марти"
>>> year = 2010
>>> print("Привет, %s! Надо отправиться в %d." % (name, 1955))
Привет, Марти! Надо отправиться в 1955.
>>> print("%(car)s использует %(fuel)s как топливо." % {"car":"DeLorean DMC-12", "fuel":"плутоний"})
DeLorean DMC-12 использует плутоний как топливо.

Строки с именоваными ключами могут показаться более громоздкими, но если часто приходится редактировать шаблоны они могут оказаться более практичными если программа выводит много текста, а словарь значений для всех строк постоянный. Или если шаблоны строк часто изменяются.

"Новый стиль" форматирования строки str.format()

Когда вышел Python 3 то в нем появился новый способ вывода строк с помощью метода строки str.format(). Он отличается тем, что вместо символа % используется конструкция из фигурных скобок {}. Имя ключа и правила вывода указываются внутри скобок, и именованные ключи передаются функции как параметры, а не как словарь:

>>> speed = 88
>>> metric_speed = speed * 1.60934
>>> print("Необходимо развить скорость в {speed:.2f} км/ч".format(speed=metric_speed))
Необходимо развить скорость в 141.62 км/ч

Позиционные аргументы могут заполнять места для подстановки по очереди:

>>> short = "Док"
>>> full = "Эмметт Браун"
>>> print("{} так же известный как {}".format(short, full))
Док так же известный как Эмметт Браун

Но позиционные аргументы в самом шаблоне могут использоваться много раз:

>>> short = "Док"
>>> print("{0} работает профессором в университете. {0} потратил все деньги на машину времени".format(short))
Док работает профессором в университете. Док потратил все деньги на машину времени

Опций форматирования вывода переменных для такого способа очень много, но зато они более однозначны и не повторяют друг-друга. Это целый микро-язык который позволяет сделать выражение состоящее из 7 компонентов, начиная от символов заполнения и стороны выравнивания и заканчивая уже известными типами переменных используемых в "старом стиле".

Посмотрите дополнительно примеры в документации:

>>> '{}, {}, {}'.format('a', 'b', 'c')
'a, b, c'
>>> '{2}, {1}, {0}'.format('a', 'b', 'c')
'c, b, a'
>>> '{2}, {1}, {0}'.format(*'abc')
'c, b, a'
>>> '{0}{1}{0}'.format('abra', 'cad')
'abracadabra'
>>> "int: {0:d};  hex: {0:x};  oct: {0:o};  bin: {0:b}".format(42)
'int: 42;  hex: 2a;  oct: 52;  bin: 101010'
>>> '{:,}'.format(1234567890)
'1,234,567,890'
>>> points = 19
>>> total = 22
>>> 'Correct answers: {:.2%}'.format(points/total)
'Correct answers: 86.36%'

В официальной документации указана причина по которой был создан новый тип форматирования строк.

Интерполяция строк

Интерполяция в данном контексте обозначат просто подстановку новых значений в старые строки. Такой формат добавлен в Python версии 3.6, который является наиболее функционально полной среди всех современных версий. И является минимальной версией для данного курса именно потому, что в нем появилась поддержка f-строк. Для того чтобы воспользоваться такой строкой вам надо всего навсего объявить переменную и в тот момент когда будет выполняться строка с шаблоном ее значение подставится в шаблон:

>>> power = 1.21
>>> print(f"Необходима мощность {power} ГВт")
Необходима мощность 1.21 ГВт

В основном синтаксис напоминает стандартный:

>>> n = 42
>>> f"int: {n:d};  hex: {n:x};  oct: {n:o};  bin: {n:b}"
'int: 42;  hex: 2a;  oct: 52;  bin: 101010'

Но появилась возможность использовать полноценные выражения внутри шаблонов, что является большим шагом вперед:

>>> year = 2015
>>> print(f"Сейчас {year}, но тебе надо вернуться в {year-30}")
Сейчас 2015, но тебе надо вернуться в 1985

Шаблоны модуля string

Еще один способ форматирования строк — это импортировать шаблоны Template и модуля string. Давайте еще раз посмотрим на пример:

>>> from string import Template
>>> t = Template('Привет, $name!')
>>> print(t.substitute(name="Бифф"))
Привет, Бифф!

В этот раз правила вставки значений немного попроще:

  • Место для подстановки обозначается символом $, после которого идет идентификатор.
  • Двойной знак доллара $$ после обработки шаблона заменяется на символ $.
  • $identifier указывает на имя ключа подстановки. Имя не зависит от регистра, допустимо использовать знак подчеркивания _, цифры и латинские буквы. Первый символ не входящий в разрешенные обозначает конец идентификатора.
  • ${identifier} полностью эквивалентно $identifier, служит для случаев когда необходимо вставить переменную туда где могут быть символы разрешенные в идентификаторах, например ${noun}ish
  • Если появится одиночный символ $ то это вызовет ошибку.

Нет дополнительных правил преобразования, выравнивания и т.д.

Зачем еще один способ?

Четвертый способ форматирования строк! Неужели не хватило предыдущих трех? Этот способ создания шаблонов был придуман для тех случаев когда вы хотите дать возможность пользователю самим указать шаблон вывода. Представьте себе что вы работаете в банке и решили сделать сервис отправки писем с напоминанием оплатить счета. И чтобы не придумывать шаблоны оформления вы сделали конкурс и пригласили дизайнеров самим оформить письма для рассылки. И вот среди дизайнеров оказался один который очень хорошо знает как работет Python. Если бы вы разрешили использовать шаблоны str.format или f-строки, то он мог бы изменить сумму на другую:

evil_template = f'Здравствуйте! Оплатите {ammount*10}'
good_template = 'Здравствуйте! Оплатите $ammount'

В случае с good_template у него не будет шанса исправить сумму.

Этот пример кажется наивным, но на самом деле обращаясь к атрибутам передаваемого объекта можно получить доступ к частям программы к которым вы бы точно не хотели чтобы пользователь обращался. Менее безобидно выглядит следующий пример (источник):

>>> SECRET = 'секретный пароль'
>>> class Msg:
...      def __init__(self):
...          pass

>>> # Мы разрешили пользователю самому указать шаблон
>>> user_input = '{message.__init__.__globals__[SECRET]}'

>>> # И вот программа выполняется чтобы отправить
>>> # пользователю сообщение:
>>> message = Message()
>>> user_input.format(message=message)
'секретный пароль'

Любое место в котором пользователи могут передать какие-то значения требует тщательной проверки. Поэтому лучше пользоваться библиотеками которые специально предназначены для обработки потенциально небезопасных данных.

Ссылки