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

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

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

это, похоже, делает то же самое, что и использование на str экземпляра. Однако я заметил, что это литеральное форматирование строк на самом деле очень медленное по сравнению с простым вызовом format. Вот что!--6--> говорит о каждом метод:

>>> 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 занимает почти половину времени. Я ожидал бы, что литеральный метод будет быстрее, потому что задействовано меньше синтаксиса. Что происходит за кулисами, что заставляет буквальный метод быть намного медленнее?

3 ответов


Примечание: этот ответ был написан для альфа-версий 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) op-коды в этом результате. Новый 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 (открыт в ответ на этот вопрос) для обсуждение способов дальнейшего повышения производительности форматированных строковых литералов.


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

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

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


этот самый вопрос был главным мотиватором для вопрос 27078 запрос Нового Кода байт-кода Python для объединения строки из фрагментов. Сергей Сторчака реализовал этот новый код и объединил его в CPython, чтобы он был доступен в Python 3.6 с версии beta 1 (и, следовательно, в Python 3.6.0 final).

в результате литеральные форматированные строки будут много быстрее 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 источников неэффективности. The __format__ метод по-прежнему не щелевой, поэтому он требует поиска словаря в классе и, таким образом, вызов его обязательно медленнее, чем зову __str__, но вызов теперь можно полностью избежать в распространенных случаях форматирования int или str экземпляры (не подклассы!) без спецификаторов форматирования.


просто обновление, отмечая, что это выглядит разрешенным в 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