Новый лучший способ форматирования строк в Python

от автора

Привет, Хабр! Приближается релиз Python 3.14, который несет нам множество нововведений. Среди них — новый способ форматирования строк. Давайте посмотрим, что из себя представляют t-строки, на что они годятся и как устроены внутри. Фича действительно мощная, будет интересно.

Что мы имеем сейчас

Прежде чем начать, предлагаю вспомнить, какие способы отформатировать текст в Python у нас уже есть:

Первый — максимально классический, используя +:

a = 1 b = "hello" print("I want to say: " + b + " (" + str(a) + ")")

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

Второй способ — %:

a = 1 b = "hello" print("I want to say: %s (%s)" % (a, b))

Очевидны многие плюсы по сравнению с предыдущим: «форматирование процентом» более лаконично. Для большего понимания того, что где будет вставляться, можно использовать имена:

print("I want to say: %(text)s (%(number)s)" % {"text": a, "number": b})

Также % позволяет делать дополнительные операции с текстом: выравнивание, заполнение (например нулями), формат чисел и фиксирование количества цифр после запятой.

Третий способ — str.format/str.format_map:

a = 1 b = "hello" print("I want to say {} ({})".format(a, b))

Имеет все те же плюсы, что и %, но в дополнение к этому имеет более мощный язык спецификации форматирования (ссылка). Тоже поддерживает именованную замену

Четвертый способ — f-строки:

a = 1 b = "hello" print(f"I want to say {a} ({b})")

По своей сути, это тот же str.format, но моментальный: в скобках записываются вычисляемые выражения, результат которых будет туда подставлен. Поддерживается весь арсенал str.format плюс к этому — специальный символ =, который работает так:

a = 1 print(f"{a=}")
>>> a=1

Важное отличие в том, что такую строку (в отличие от двух предыдущих способов) нельзя заранее подготовить, а отформатировать когда-нибудь потом: все выражения eagerly-evaluated.

Пятый способ — довольно забытый string.Template:

from string import Template a = 1 b = "hello" t = Template("I want to say $a ($b)") print(t.substitute({"a": a, "b": b}))

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

Синтаксис и поведение t-строк

Теперь посмотрим на синтаксис t-строк. По факту, он никак не отличается от синтаксиса f-строк, кроме, собственно, префикса. Выражения в скобках имеют такую же структуру: {выражение[!конверсия][:формат]}. Выражения все так же не ленивые.

Однако, t-строки достаточно сильно отличаются в плане поведения форматирования. Если мы попробуем написать что-то вроде print(t"Hello, {name}"), то увидим не строку, а какой-то объект Template. Давайте разбираться.

Template

Объекты этого класса создаются в результате вычисления t-строки. Что внутри имеет Template?

dir(t"") >>> ['__add__', '__class__', '__class_getitem__', '__delattr__',       '__dir__', '__doc__', '__eq__', '__format__', '__ge__',       '__getattribute__', '__getstate__', '__gt__', '__hash__',       '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__',       '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',       '__setattr__', '__sizeof__', '__str__', '__subclasshook__',       'interpolations', 'strings', 'values']

Из всех этих атрибутов нас интересуют только interpolations, strings, values и __iter__.

В атрибуте strings лежат все строковые значения, которые лежат между интерполяциями (интерполяция в нашем случае — {выражение}). Например, t"Hello, {name}!".strings == ('Hello, ', '!', '').

В атрибуте values лежат все значения интерполяций. Например, t"{1}".values == (1,).

В атрибуте interpolations лежат все интерполяции, представленные объектами Interpolation. Он содержит в себе поля:

  • value — само посчитанное значение интерполяции;

  • expression — строка с исходным кодом выражения;

  • conversion — тип конверсии с помощью ! (!r, !a или !s). Если конверсия не указана, то содержит пустую строку;

  • format_spec — спецификация форматирования с помощью :. Если спецификация не указана, то содержит пустую строку.

Например, t"{1+2!a:ffff}".interpolations вернет (Interpolation(3, '1+2', 'a', 'ffff'),).

Наконец, __iter__ позволяет нам итерироваться по t-строке. Во время итерации мы сможем по порядку перебирать все ее элементы: строки и интерполяции.

В чем соль?

«В чем же собственно фишка t-строк?» — спросите вы. Главная фишка в том, что форматирование происходит не сразу, а вместо этого вам отдается объект, который содержит полную информацию о строке, чтобы вы потом отформатировали эту строку сами ровно так, как пожелаете. То есть, вся суть — кастомизация поведения форматирования.

Для примера давайте попробуем сделать f-строки на t-строках. Сделаем функцию f:

from string.templatelib import Interpolation  def f(template):     string = []     for part in template:         match part:         case str():                 string.append(part)             case Interpolation(value, _, conversion, format_spec):                 if conversion == "a":                     value = ascii(value)                 elif conversion == "s":                     value = str(value)                 elif conversion == "r":                     value = repr(value)                 value = format(value, format_spec)                 string.append(value)     return "".join(string)

Не самый красивый код, но это всего лишь пример. Мы просто проходим по всем частям, если надо — форматируем, а затем добавляем к строке.

Теперь давайте рассмотрим, чем эта штуковина может быть нам полезна. Способы применения я условно поделил на 3 группы. Некоторые из сфер применения могут быть сомнительными, пишите свое мнение в комментарии, будет интересно прочитать)

Санитизация

Один из основных аргументов «ЗА» PEP750, который привнес нам эти t-строки — упрощение защиты от XSS и SQL injection атак. Каким образом? Если раньше мы попробовали бы сделать что-то такое:

sql.execute(f"SELECT * FROM users WHERE username = '{username}';")

то нам бы закономерно на ревью дали по шапке. Ведь если у нас придет пользователь с юзернеймом '; DROP TABLE users; --, то будет очень и очень больно. Но если мы будем использовать t-строки, то наш гипотетический sql.execute сможет пройтись по всем строковым значениям в интерполяциях и сам их экранировать:

# вымышленный коннектор к бд import my_awesome_sql_library  def execute(query): ... my_awesome_sql_library.execute(sanitize_sql(query)) ...  def sanitize_sql(query): string = []     for part in template:         match part:         case str():                 string.append(part)             case Interpolation(value, _, conversion, format_spec):                 if conversion == "a":                     value = ascii(value)                 elif conversion == "s":                     value = str(value)                 elif conversion == "r":                     value = repr(value)                 value = format(value, format_spec)                 string.append(my_awesome_sql_library.sanitize(value))     return "".join(string)

Мнение автора

Не совсем ясно, насколько такой подход лучше уже существующего, когда мы передаем коннектору строку с плейсхолдерами типа % и значения, а БД их сама экранирует и заменяет.

Шаблонизация

Ровно то же самое применимо и к HTML. Во-первых, это помогает разобраться с XSS: если мы собираем какую-то страничку с помощью f-строк, например:

# вымышленный веб-фреймворк  def post_html(post): return f""" <div class="post"> <h3>{post.title}</h3> <small>{post.author}</small> <a href="/posts/{post.id}">See</a> </div> """  @get("/posts") def posts_page(request): posts = get_all_posts() return f""" <h1>Posts</h1> {"".join(map(post_html, posts))} """

и какой-нибудь особо любопытный пользователь сделает пост с заголовком <script>alert("There I am!")</script> то мы получим XSS. Но мы можем написать все то же самое с использованием t-строк, а фреймворк будет просто делать html.escape(...) для всех интерполяций, таким образом избегая инъекции HTML кода.

Во-вторых, мы можем делать кучу разных дополнительных манипуляций с HTML кодом. Например, парсить его прямо во время форматирования. Давайте взглянем на несколько примеров, которые написал Дейв Пек — один из авторов PEP750:

text = 'Hello, "world!"' element = html(t'<p class="greeting">{text}</p>') expected = Element("p", {"class": "greeting"}, ['Hello, "world!"']) assert element == expected

или такое:

def Magic(attributes, children): """A simple, but extremely magical, component.""" magic_attributes = {**attributes, "data-magic": "yes"} magic_children = [*children, "Magic!"] return Element("div", magic_attributes, magic_children)  element = html(t'&lt;{Magic} id="wow"&gt;<b>FUN!</b>') expected = Element( "div", {"id": "wow", "data-magic": "yes"}, [Element("b", {}, ["FUN!"]), "Magic!"], ) assert element == expected

В эфире на тему PEP750 его создатели (Джим Бейкер, Дейв Пек, Пол Эверитт) обсуждали много разных вещей, для которых он нужен. Среди прочего, интересна такая цитата (перевод):

… В то время бытовало мнение, что HTML довольно сильно отличается от программ …
… вам хотелось разделять людей, работающих с шаблонами, от людей, пишущих ПО …
… мир так больше не работает …
… это то, в чем я изначально был заинтересован …
… мы хотим DSL-ы, которые ближе к Python, чтобы вы могли использовать mypy для них, свой форматтер для них, подсказки своей среды разработки для них …
… и, с точки зрения HTML, мы хотим перейти из этого разделения в новый мир.

По сути, они предлагают новый способ шаблонизации. Цитата с переводом:

— … новые способы шаблонизации, например jinja3 или что-нибудь еще, могут появиться и развить эти идеи, используя что-то отсюда …
— Да, абсолютно точно, и я надеюсь, что мы увидим бум таких вещей после выхода Python 3.14 …

Мнение автора

Эти идеи кажутся интересными, возможно даже и действительно начнут какое-то новое движение в SSR в мире Python, но пока об этом рано говорить до выхода 3.14 в октябре и появления первых инструментов на основе t-строк. Да и нужно проводить тщательные сравнения и бенчмарки.

Структурированное логирование

Структурированное логирование позволяет делать логи в машиночитаемом формате. Традиционный подход — использование structlog/loguru (иногда даже можно использовать обычный встроенный logging). Все так или иначе сводится к тому, что у нас есть какой-то словарик значений, который мы куда-то передаем и он становится частью структурированного сообщения в логе.

T-строки позволяют сделать структурный логгинг фактически бесплатным. Предлагаю такой пример:

logger.info(t"Received request {request['id']:id} from user {request['user']:user}")  # string formatting >>> Received request 123 from user 0000-0000 # json formatting >>> {   "id": 123,    "user": "0000-0000",    "message": "Received request 123 from user 0000-0000",    "type": "received_request_from_user",    "time": "2025-05-21T13:19:14" }

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

id = request['id'] user = request['user'] logger.info(t"Received request {id} from user {user}")

Затем мы добавляем поле time — время создания сообщения и message — само сообщение. Тут необходимо еще отметить то, что для структурного логгинга также нужно, чтобы сообщения имели какой-то идентификатор, который бы позволял различать события. Здесь же этим идентификатором служит строка, в которой все интерполяции заменены на ?. Потом, где-нибудь в дашборде, где мы смотрим все логи, мы сможем сделать запрос вида: (loki) {app=my_app,type=received_request_from_user}.

Что можно сделать еще? Продолжить расширять формат-спецификацию. Так как мы заняли ее место, надо добавить возможность дописывать обычную format spec. Это можно сделать, например, так:

x = 13.00000000000004 logger.info(t"Some number: {x:my_field+.4f}")  # string formatting >>> Some number: 13.0000 # json formatting >>> {..., "my_field": 13.00000000000004, ...}

Можем продолжить расширять формат-спеку. Допустим, можно добавить возможность указывать форматирование для дат (хотя, скорее это уже перебор):

logger.info(t"Current hour: {datetime.now():now+%Y-%m-%d %H}")

То, как происходит форматирование таких логов под капотом, можно посмотреть в репозитории с примерами.

Мнение автора

Как по мне, то это наиболее перспективное направление, в котором можно использовать t-строки. Совокупность факторов: лаконичность (не надо ссылаться на один и тот же объект и в сообщении, и в словарике со значениями) и бесплатные автогенерируемые идентификаторы типа сообщения — дают некоторое преимущество над стандартным подходом вида

logger.info(   "Request {id} from {user} received",    {"id": request.id, "user": request.user, "type": "received_request_from_user"} )

Тем не менее, не стоит забывать, что t-строки заметно медленнее, чем стандартный str.format, да и с существующим подходом все замечательно пока живут, хотя с t-строками и получается сильно красивее.

Что внутри?

Для полноты картины нам не хватает только части с разбором того, что находится под капотом у t-строк.

Давайте посмотрим байткод для такого выражения:

t"a {1} b {2!s:fmt} c"
LOAD_CONST               4 (('a ', ' b ', ' c')) LOAD_SMALL_INT           1 LOAD_CONST               1 ('1') BUILD_INTERPOLATION      2 LOAD_SMALL_INT           2 LOAD_CONST               2 ('2') LOAD_CONST               3 ('fmt') BUILD_INTERPOLATION      7 BUILD_TUPLE              2 BUILD_TEMPLATE

Первым делом видим создание кортежа из чистых строк, которые находятся между интерполяциями. Затем видим загрузку на стек значения первой интерполяции и строки "1" — ее исходного кода, после чего опкод BUILD_INTERPOLATION создает объект Interpolation.

После видим, как загружается вторая интерполяция, ее исходный код, но теперь еще и строка "fmt", которую мы указали в качестве спецификации форматирования. А затем опять вызывается BUILD_INTERPOLATION, но уже с другим оп-аргументом. Если мы посмотрим на код:

inst(BUILD_INTERPOLATION, (value, str, format[oparg &amp; 1] -- interpolation)) {       PyObject *value_o = PyStackRef_AsPyObjectBorrow(value);       PyObject *str_o = PyStackRef_AsPyObjectBorrow(str);       int conversion = oparg &gt;&gt; 2;       PyObject *format_o;       if (oparg &amp; 1) format_o = PyStackRef_AsPyObjectBorrow(format[0]);       else format_o = &amp;_Py_STR(empty);      PyObject *interpolation_o = _PyInterpolation_Build(value_o, str_o, conversion, format_o);      ...     interpolation = PyStackRef_FromPyObjectSteal(interpolation_o);   } 

то увидим, что оп-аргумент контроллирует наличие/отсутствие спецификации форматирования, а также конверсии (!a, !s или !r) с помощью булевых флагов. Здесь же видно, что при отсутствии спецификации форматирования, она принимает дефолтное значение пустой строки.

Следующий опкод — BUILD_TUPLE 2 — создает кортеж из двух значений на стеке. Эти значения — сформированные интерполяции. После этого опкод BUILD_TEMPLATE просто собирает вместе кортежи чистых строк и интерполяций в шаблон.

inst(BUILD_TEMPLATE, (strings, interpolations -- template)) {       PyObject *strings_o = PyStackRef_AsPyObjectBorrow(strings);       PyObject *interpolations_o = PyStackRef_AsPyObjectBorrow(interpolations);       PyObject *template_o = _PyTemplate_Build(strings_o, interpolations_o);       ...     template = PyStackRef_FromPyObjectSteal(template_o);   }

Строки и интерполяции чередуются, т.е., например, кортеж чистых строк у шаблона t"{a}{b}" будет содержать ("", "", "").

Заключение

T-строки — крайне мощный инструмент, который вот-вот (уже в октябре) будет доступен в стабильном виде в Python 3.14. Хотя нам и только предстоит увидеть их в действии в реальных юзкейсах, уже сейчас можно сказать, что они определенно точно привнесут кое-что новое и удобное в мир шаблонизации и структурного логирования.

Спасибо Никите Соболеву (CPython Core Developer) за пруфридинг статьи и особенно секции с внутренностями t‑строк. Еще больше приколов про внутрянку CPython и свежие новости про новые фичи вы можете узнать у него на канале «Находки в опенсорсе».

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вам t-строки?

14.47% Очень интересно и полезно, точно буду использовать45
55.95% Интересно, но не понятно, чем они лучше существующих способов174
29.58% Какая-то совсем не нужная ерунда, засоряющая язык92

Проголосовали 311 пользователей. Воздержались 69 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/911196/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *