В Python есть 3 способа форматировать строки, и один из них лучше других. Но не будем забегать наперед — о каком именно форматировании вообще речь? Каждый раз когда мы хотим поприветствовать пользователя по имени нам нужно вставить строку с именем в строку-шаблон. Большинство полезных записей в логах так же содержат значения переменных. И вот пример:

integer = 42
string = 'FORTY_TWO'

print('string number %s, or simply %d' % (string, integer))
print('string number {}, or simply {}'.format(string, integer))
print(f'string number {string}, or simply {integer}')

Первый способ, форматирование оператором %, пришел в Python еще из С — он имитирует функцию printf. Этот способ был первым в питоне, и остается единственным (из обсуждаемых в статье) в Python версии 2.5 и ниже.

Второй способ — это метод str.format, принадлежащий встроенному классу строк. Он появился с Python 3.0, и был портирован в версию 2.6. Этот метод был рекомендован как обладающий более богатым синтаксисом.

Третий способ, f-string, появился в Python версии 3.6. Как объяснено в PEP-0498, создание нового способа форматирования строк было мотивировано недостатками существующих методов, которые авторы характеризуют как подверженные ошибкам, недостаточно гибкие и не элегантные:
This PEP is driven by the desire to have a simpler way to format strings in Python. The existing ways of formatting are either error prone, inflexible, or cumbersome.
Итак, у нас есть три способа решить одну задачу. Но может это дело личного вкуса и предпочтений? Возможно, но стиль вашего кода (особенно кода в проекте с большим количеством участников) точно выиграет от единообразия. В лучшем случае стоит использовать один метод форматирования строк, тогда читать код станет проще. Но какой же метод выбрать? И есть ли разница в производительности кода?

Попробуем ответить на вопрос о производительности экспериментально:

import timeit

setup = """
integer = 42
string = 'FORTY_TWO'
""".strip()

percent_stmt ="'Number %s or simply %d' % (string, integer)"
call_stmt = "'Number {} or simply {}'.format(string, integer)"
fstr_stmt = """f'Number {string} or simply {integer}'"""


def time(stmt):
    return f"{timeit.timeit(stmt, setup, number=int(1e7)):.3f}"

print(f"Timing percent formating:    |  {time(percent_stmt)}")
print(f"Timing call formating:       |  {time(call_stmt)}")
print(f"Timing f-string formating:   |  {time(fstr_stmt)}")

Результаты на мак-буке с Python 3.7:

Timing percent formating:    |  2.025
Timing call formating:       |  2.943
Timing f-string formating:   |  1.348

Разница значительная. И что же теперь, запускать поиск regex на ".format" и переписывать сотни выражений? В принципе задача простая, но трудоемкая. Плюс вероятность допустить ошибку и посадить баг в до этого работающий код! Кажется, есть место для автоматизации. И действительно, существуют библиотеки способные конвертировать большинство выражений в f-strings: flynt, pyupgrade.

Flynt прост в использовании. К примеру, запустим конвертацию на исходном коде flask:

38f9d3a65222:~ ikkamens$ git clone https://github.com/pallets/flask.git
Cloning into 'flask'...
...
Resolving deltas: 100% (12203/12203), done.

38f9d3a65222:~ ikkamens$ flynt flask

Flynt run has finished. Stats:

Execution time: 0.623s
Files modified: 18
Expressions transformed: 43
Character count reduction: 241 (0.04%)

_-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_.

Please run your tests before commiting. Report bugs as github issues at: https://github.com/ikamensh/flynt
Thank you for using flynt! Fstringify more projects and recommend it to your colleagues!

_-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_._-_.
38f9d3a65222:~ ikkamens$

Так же стоит отметить возможность конвертации выражений, занимающих несколько строк, и сбор статистики о выполненных изменениях. Флаг --line_length XX определяет лимит длины строки после преобразования. Flynt позволяет вызвать pyupgrade с флагом --upgrade.

Pyupgrade включает в себя больше функционала, и может почистить ваш код от многих артефактов Python 2 — таких как наследование от object, указание имен классов в super и многое другое. Pyupgrade задуман для использования с pre-commit, утилитой для автоматической модификации кода перед коммитами.

Конвертировать лучше исходники в гите или другом контроле версий. Стоит прогнать тесты и посмотреть на изменения самому (используя git diff или среды типа PyCharm). Покуда среди нас живы те, кому не все равно, что код стал на пару символов короче, проактивная конвертация также сэкономит их время. Ведь рано или поздно кто-то начнет делать руками то, что можно сделать утилитой. F-strings работают только на Python 3.6+, но скоро это не будет проблемой так как другие версии устареют.

Стоит отметить что совсем отказаться от классического метода .format не получится. В случае когда вы используете один и тот же шаблон для создания сообщений с разными переменными в разных местах кода следует сохранить этот шаблон в переменной, и использовать её — принцип «Don't repeat yourself» куда важнее чем выигранные наносекунды от форматирования строки.

Выводы:

Мы рассмотрели три способа форматирования строк, доступные в версиях Python 3.6+, их краткую историю и сравнили их производительность. Мы также рассмотрели существующие в открытом доступе утилиты для автоматической конвертации кода к новому методу форматирования строк, и их дополнительные функции. Не забывайте о простых вещах в вашем коде, и удачи!

Комментарии (11)


  1. frostspb
    01.08.2019 16:40
    +1

    Не стоит бездумно кидать рефакторить.
    В книге «Чистый питон» есть такая рекомендация:

    Если форматирующие строки поступают от пользователей, то используйте шаблонные строки, чтобы избежать проблем с безопасностью.
    В противном случае используйте интерполяцию литеральных строк при
    условии, что вы работаете с Python 3.6+, и «современное» форматирование строк — если нет.

    Здесь под интерполяцией литеральных строк, автор имеет в виду f-string
    Под «современным» понимается: 'Привет, {}'.format(name)
    >>> SECRET = 'это – секрет'
    >>> class Error:
    ... def __init__(self):
    ... pass
    >>> err = Error()
    >>> user_input = '{error.__init__.__globals__[SECRET]}'
    # Ой-ей-ей...
    >>> user_input.format(error=err)
    'это – секрет'



    защита

    
    >>> user_input = '${error.__init__.__globals__[SECRET]}'
    >>> Template(user_input).substitute(error=err)
    ValueError:
    "Invalid placeholder in string: line 1, col 1" 
    


    1. BasicWolf
      01.08.2019 19:33

      Армин тоже разбирал этот момент: http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/


    1. YuriM1983
      02.08.2019 14:16

      Я может что-то не понимаю, но использовать пользовательский ввод в качестве шаблона строк — это потенциальная уязвимость со времен функций *prinf в C.
      Не надо так.
      PS: лично я использую str.replace и re.sub в этих случаях.


      1. Mingun
        03.08.2019 18:09

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


    1. asi81
      04.08.2019 11:55
      +1

      Довольно интересный пример, но, как я понял, flynt меняет строки на fстроки, которые являются константными литералами, тоесть подобная уязвимость к ним не применима.


  1. Sly_tom_cat
    02.08.2019 12:09

    F-strings работают только на Python 3.6+, но скоро это не будет проблемой так как другие версии устареют.

    Согласно: devguide.python.org/#status-of-python-branches
    2.7 — да 2020-01-01 — RIP.
    Но 3.5 — еще до 2020-09-13.

    Поэтому я бы не торопился с переводом питоновского кода (даже если он уже на под 3+ версию заточен) на фичи, которые поддерживаются только в 3.6+

    Я бы подождал хотя бы еще пол годика после 2020-09-13 да и то в некоторых местах 3.5 и ниже могут еще довольно долго жить.


  1. shaukote
    02.08.2019 15:34

    Спасибо за статью, я вот как-то упустил появление f-string. Хотя хотелось бы увидеть более полное раскрытие темы — для меня осталось непонятным, почему f-string такие быстрые.


    1. ikamensh Автор
      04.08.2019 12:04

      Если найду время покопаю глубже и напишу еще статью. Что я уже знаю: f-strings не парсят шаблон для того, чтобы найти куда вставить переменную — в Abstract Syntax Tree они называются JoinedString, и содержал лист выражений, которые будут соединены в одну строку. Например f«Hello {world}» образует лист из [ast.Str ('Hello '), ast.FormattedValue (value=world)]. F-strings парсятся только один раз при начальной интерпретации файла. Метод .format получает строку как аргумент и парсит каждый раз, а на f-string возможны оптимизации (исходник превратится в .pyc).


      1. shaukote
        05.08.2019 16:59

        Спасибо, в целом я понял из этого описания. :)


  1. snamef
    04.08.2019 11:55

    прикольно, то что в скобках просто исполняется поэтому должны быть защищено от XXS атак на питон))


  1. black_semargl
    05.08.2019 18:30

    Кстати, f-strings могут быть вложенными

    f'''-{f"""*{f"+{f'.{x}.'}+"}*"""}-'''