Подтвердить что ты не робот

Почему строки в буквальном формате (f-строки) были такими медленными в Python 3.6 alpha? (сейчас исправлено в 3.6 стабильной)

Я загрузил альфа-версию Python 3.6 из репозитория Python Github, и одна из моих любимых новых функций - это буквальное форматирование строк. Его можно использовать так:

>>> x = 2
>>> f"x is {x}"
"x is 2"

Это похоже на то, что используется функция format на экземпляре str. Тем не менее, одна вещь, которую я заметил, это то, что это буквальное форматирование строки на самом деле очень медленное по сравнению с просто вызовом format. Здесь timeit говорит о каждом методе:

>>> x = 2
>>> timeit.timeit(lambda: f"X is {x}")
0.8658502227130764
>>> timeit.timeit(lambda: "X is {}".format(x))
0.5500578542015617

Если я использую строку как аргумент timeit, мои результаты все еще показывают шаблон:

>>> timeit.timeit('x = 2; f"X is {x}"')
0.5786435347381484
>>> timeit.timeit('x = 2; "X is {}".format(x)')
0.4145195760771685

Как вы можете видеть, использование format занимает почти половину времени. Я ожидал бы, что литеральный метод будет быстрее, потому что задействован меньший синтаксис. Что происходит за кулисами, из-за чего литеральный метод становится намного медленнее?

4b9b3361

Ответ 1

Примечание. Этот ответ был написан для альфа-версий Python 3.6. Новый код операции, добавленный к 3.6.0b1, значительно улучшил производительность f-строки.


Синтаксис f"..." эффективно преобразуется в str.join() над частями литеральной строки вокруг выражений {...}, а результаты самих выражений передаются через метод object.__format__() ( передача любого :.. спецификация формата в). Вы можете увидеть это при разборке:

>>> import dis
>>> dis.dis(compile('f"X is {x}"', '', 'exec'))
  1           0 LOAD_CONST               0 ('')
              3 LOAD_ATTR                0 (join)
              6 LOAD_CONST               1 ('X is ')
              9 LOAD_NAME                1 (x)
             12 FORMAT_VALUE             0
             15 BUILD_LIST               2
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 POP_TOP
             22 LOAD_CONST               2 (None)
             25 RETURN_VALUE
>>> dis.dis(compile('"X is {}".format(x)', '', 'exec'))
  1           0 LOAD_CONST               0 ('X is {}')
              3 LOAD_ATTR                0 (format)
              6 LOAD_NAME                1 (x)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 POP_TOP
             13 LOAD_CONST               1 (None)
             16 RETURN_VALUE

Обратите внимание на BUILD_LIST LOAD_ATTR.. (join) BUILD_LIST и LOAD_ATTR.. (join) в этом результате. Новый FORMAT_VALUE берет вершину стека плюс значение формата (анализируется во время компиляции), чтобы объединить их в вызове object.__format__().

Итак, ваш пример, f"X is {x}", переводится на:

''.join(["X is ", x.__format__('')])

Обратите внимание, что для этого требуется Python для создания объекта списка и вызова str.join().

Вызов str.format() также является вызовом метода, и после синтаксического анализа все еще остается вызов x.__format__(''), но, что важно, здесь нет создания списка. Именно это различие ускоряет метод str.format().

Обратите внимание, что Python 3.6 был выпущен только как альфа-сборка; эта реализация все еще может легко измениться. См. PEP 494 - Расписание выпуска Python 3.6 для расписания, а также выпуск Python # 27078 (открыт в ответ на этот вопрос) для обсуждения того, как еще больше повысить производительность форматированных строковых литералов.

Ответ 2

До версии 3.6 beta 1 строка формата f'x is {x}' была скомпилирована в эквивалент ''.join(['x is ', x.__format__('')]). Полученный код был неэффективным по нескольким причинам:

  1. он построил последовательность строковых фрагментов...
  2. ... и эта последовательность была списком, а не кортежем! (Построить кортежи немного быстрее, чем списки).
  3. он поместил пустую строку в стек
  4. он посмотрел метод join на пустую строку
  5. он вызывал __format__ даже для __format__('') объектов Unicode, для которых __format__('') всегда возвращал бы self или целочисленные объекты, для которых __format__('') в качестве аргумента возвращал str(self).
  6. Метод __format__ не имеет __format__.

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


Именно этот вопрос был основным мотиватором для вопроса 27078, запрашивающего новый код операции байт-кода Python для фрагментов строки в строку (код операции получает один операнд - число фрагментов в стеке; фрагменты вставляются в обратном порядке, т.е. последний часть является самым верхним элементом). Сергей Сторчака реализовал этот новый код операции и объединил его в CPython, чтобы он был доступен в Python 3.6 начиная с бета-версии 1 (и, следовательно, в Python 3.6.0 final).

В результате строки, отформатированные в буквальном string.format будут намного быстрее, чем string.format. Они также часто намного быстрее, чем старое форматирование в Python 3.6, если вы просто интерполируете объекты str или int:

>>> timeit.timeit("x = 2; 'X is {}'.format(x)")
0.32464265200542286
>>> timeit.timeit("x = 2; 'X is %s' % x")
0.2260766440012958
>>> timeit.timeit("x = 2; f'X is {x}'")
0.14437875000294298

f'X is {x}' теперь компилируется в

>>> dis.dis("f'X is {x}'")
  1           0 LOAD_CONST               0 ('X is ')
              2 LOAD_NAME                0 (x)
              4 FORMAT_VALUE             0
              6 BUILD_STRING             2
              8 RETURN_VALUE

Новый BUILD_STRING вместе с оптимизацией в коде FORMAT_VALUE полностью устраняет первые 5 из 6 источников неэффективности. Метод __format__ прежнему не имеет __format__, поэтому он требует поиска в классе по словарю и, следовательно, его вызов обязательно медленнее, чем вызов __str__, но теперь можно полностью избежать вызова в обычных случаях форматирования экземпляров int или str (не подклассы) !) без форматирования спецификаторов.

Ответ 3

Просто обновление, отмечающее, что это похоже на разрешение в выпуске Python3.6.

>>> import dis
>>> dis.dis(compile('f"X is {x}"', '', 'exec'))
  1           0 LOAD_CONST               0 ('X is ')
              2 LOAD_NAME                0 (x)
              4 FORMAT_VALUE             0
              6 BUILD_STRING             2
              8 POP_TOP
             10 LOAD_CONST               1 (None)
             12 RETURN_VALUE

>>> dis.dis(compile('"X is {}".format(x)', '', 'exec'))
  1           0 LOAD_CONST               0 ('X is {}')
              2 LOAD_ATTR                0 (format)
              4 LOAD_NAME                1 (x)
              6 CALL_FUNCTION            1
              8 POP_TOP
             10 LOAD_CONST               1 (None)
             12 RETURN_VALUE