4 основных возможностей форматирования строк в Python
В этой статье будет продемонстрировано, как работают четыре принципа форматирования строк в Python и какие у них сильные и слабые стороны. Также будет предложена простая рекомендация по выбору наилучшего способа форматирования. Предположим, что у нас есть переменные (или константы):
>>> errno = 50159747054
>>> name = 'Gvido'
И на основе этих переменных нужно сгенерировать строку вывода, содержащую простое сообщение об ошибке:
'Hey Gvido, there is a 0xbadc0ffee error!'
Далее будут представлены варианты решения данной задачи, с применением форматирования строк.
1 – «Старый стиль» форматирования строк
Строки в Python содержат встроенный оператор, к которому можно получить доступ с помощью символа %. Он позволяет легко упростить позиционное форматирование. Если раньше вы работали с функцией printf в C, то простой пример далее будет понятен мгновенно:
>>> 'Hello, %s' % name
"Hello, Gvido"
Здесь используется спецификатор формата %s, для указания Python, где заменить значение имени, представленного в виде строки.
Существуют и другие спецификаторы формата, которые позволяют управлять форматом вывода. Например, можно преобразовать числа в шестнадцатеричную систему счисления или добавить пробелы при генерации красиво отформатированных таблиц и отчетов.
Можно также использовать спецификатор формата % для преобразования значения int в строку представленное как шестнадцатеричное число:
>>> '%x' % errno
'badc0ffee'
Синтаксис форматирования строки «старого стиля» немного меняется, если нужно сделать несколько подстановок в одной строке. Поскольку оператор % принимает только один аргумент, нужно обернуть правую часть переменных в кортеж.
>>> 'Hey %s, there is a 0x%x error!' % (name, errno)
'Hey Gvido, there is a 0xbadc0ffee error!'
Также можно ссылаться на имена переменных в строке форматирования, если их передать в шаблон через словарь:
>>> 'Hey %(name)s, there is a 0x%(errno)x error!' % {
... "name": name, "errno": errno}
'Hey Gvido, there is a 0xbadc0ffee error!'
Это упрощает процесс форматирования и ускоряет его модификацию в будущем. Не нужно беспокоиться о том, что порядок, который передаётся в значениях, совпадает с порядком, на который ссылаются значения в строке формата. Конечно, недостатком является то, что этот метод требует немного большего набора текста.
После прочтения у вас может возникнуть вопрос, почему форматирование в стиле printf называется форматированием строки «старого стиля»? Потому что оно было технически заменено форматированием «нового стиля», о котором будет рассказано далее.
2 – «Новый стиль» форматирования строк
Python 3 представил новый способ создания строкового форматирования, который также был позже бэкпортирован в Python 2.7. Форматирование строки «нового стиля» избавляется от специального синтаксиса оператора %, превращая синтаксис форматирования строк более регулярным. Теперь форматирование выполняется путем вызова функции format() для строкового объекта.
Также можно использовать функцию format() для простого позиционного форматирования, как и при форматировании «старого стиля»:
>>> 'Hello, {}'.format(name)
'Hello, Gvido'
Или ссылаться на подстановки переменных по имени и использовать их в любом порядке. Это довольно мощная функция, поскольку она позволяет повторно упорядочить порядок отображения без изменения аргументов, переданных функции format:
>>> 'Hey {name}, there is a 0x{errno:x} error!'.format(
... name=name, errno=errno)
'Hey Gvido, there is a 0xbadc0ffee error!'
Этот пример также показывает, изменённый синтаксис для форматирования переменной int как шестнадцатеричной строки. Теперь нужно передать спецификацию формата, добавив суффикс: x. Синтаксис строки формата стал более мощным, не усложняя более простые варианты использования.
В Python 3 форматирование строки «нового стиля» должно быть предпочтительнее, чем форматирование в стиле %. Хотя форматирование «старого стиля» считается устаревшим, но оно не удалено и по-прежнему поддерживается в крайних версиях Python. Если ссылаться на обсуждение в списках рассылки Python dev и освещение этой проблемы в bug трекере для разработчиков Python, оператор форматирования % будет присутствовать ещё долгое время.
Тем не менее, официальная документация Python 3.X не рекомендует использовать форматирование «старого стиля» и сдержанно сообщает:
«Описанные здесь операции форматирования демонстрируют множество причуд, которые приводят к ряду распространенных ошибок (например, при неправильном отображении кортежей и словарей). Использование новых форматированных строковых литералов или интерфейса str.format() помогает избежать этих ошибок. Эти альтернативы также обеспечивают более мощные, гибкие и расширяемые подходы к форматированию текста.»
Вот почему рекомендуется придерживаться str.format при написании нового кода. Начиная с Python 3.6 есть ещё один способ форматирования строк. Об этом будет рассказано в следующем разделе.
3 – Литеральная интерполяция строки (Python 3.6+)
Python 3.6 добавляет новый метод форматирования строк под названием Форматированные строковые литералы. Это новый способ форматирования строк, позволяющий использовать встроенные выражения Python внутри строковых констант. Вот простой пример, который поможет почувствовать эту функцию:
>>> f'Hello, {name}!'
'Hello, Gvido!'
Новый синтаксис форматирования является очень мощным, потому что может содержать произвольные Python выражения с внутренней арифметикой. Например:
>>> a = 5
>>> b = 10
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.'
'Five plus ten is 15 and not 30.'
Форматированные строковые литералы – это функция парсера Python, которая преобразует f-string в ряд строковых констант и выражений. Затем они соединяются для построения финальной строки.
Допустим, что есть функция greet(), которая содержит f-string:
def greet(name, question):
return f"Hello, {name}! How it {question}?"
greet('Gvido', 'going')
"Hello, Gvido! How
it going?"
Если рассмотреть функцию более подробно, можно увидеть, что f-string в функции преобразуется в следующее:
def greet(name, question):
return "Hello, " + name + "! How it " + question + "?"
Реальная реализация немного быстрее, чем при использовании кода операции BUILD_STRING в качестве оптимизации. Но функционально они одинаковы:
>>> import dis
>>> dis.dis(greet)
2 0 LOAD_CONST 1 ('Hello, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 LOAD_CONST 2 ("! How it ")
8 LOAD_FAST 1 (question)
10 FORMAT_VALUE 0
12 LOAD_CONST 3 ('?')
14 BUILD_STRING 5
16 RETURN_VALUE
Строковые литералы также поддерживают существующий синтаксис формата строки метода str.format(). Это позволяет решать те же проблемы форматирования, которые обсуждались в предыдущих двух разделах:
>>> f"Hey {name}, there's a {errno:#x} error!"
"Hey Gvido, there's a 0xbadc0ffee error!"
Новое форматирование строковых литералов Python похожи на литералы шаблонов в JavaScript, добавленные в ES2015.
4 – Template Strings (стандартная библиотека)
Ещё одним способом форматирования строк в Python является Template Strings. Он более простой и менее мощный механизм, но в некоторых случаях он может быть предпочтительным.
Давайте рассмотрим простой пример приветствия:
>>> from string import Template
>>> t = Template('Hey, $name!')
>>> t.substitute(name=name)
'Hey, Gvido!'
Первоначально нужно импортировать класс Template из встроенного модуля string. Template strings не является основным синтаксисом, но при этом поставляются модулем стандартной библиотеки.
Другое отличие состоит в том, Template strings не поддерживает спецификаторы формата. Поэтому, чтобы заработал пример строки с сообщением о ошибке, нам нужно преобразовать номер ошибки int в hex строку:
>>> templ_string = 'Hey $name, there is a $error error!'
>>> Template(templ_string).substitute(
... name=name, error=hex(errno))
'Hey Gvido, there is a 0xbadc0ffee error!'
Итак, возникает вопрос, когда использовать Template strings в Python программах? На мой взгляд, наилучшим вариантом использования Template strings является случай обработки пользовательских строк форматирования. Из-за незначительной сложности Template strings являются более безопасным выбором.
Более сложные мини-языки форматирования других методов форматирования строк могут привести к уязвимостям системы безопасности программ. Например, для строк с методом format можно получить доступ к произвольным переменным вашей программы.
Это означает, что если злоумышленник может предоставить строку формата, он может потенциально украсть секретные ключи и другую деликатную информацию! Вот простое доказательство того, как можно реализовать эту атаку:
>>> SECRET = 'this-is-a-secret'
>>> class Error:
... def __init__(self):
... pass
>>> err = Error()
>>> user_input = '{error.__init__.__globals__[SECRET]}'
>>> user_input.format(error=err)
'this-is-a-secret'
Посмотрите, как гипотетический злоумышленник смог извлечь нашу секретную строку, обратившись к словарю globals? Страшно, да? Template strings закрывают этот вектор атаки. И это делает их более безопасным выбором, если обрабатываются строки форматирования, созданные из пользовательского ввода:
>>> user_input = '${error.__init__.__globals__[SECRET]}'
>>> Template(user_input).substitute(error=err)
ValueError:
"Invalid placeholder in string: line 1, col 1"
Какой метод форматирования строк следует использовать?
Возможно вы сейчас в замешательстве, какой способ форматирования строк в Python выбрать. Далее представлена блок-схема, которая поможет определиться.