2016-05-21 7 views
26

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

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

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

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

Если я использую строку в качестве аргумента timeit «s, мои результаты по-прежнему показывает картину:

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

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

+0

f-строки являются динамическими, поэтому строка должна генерироваться в каждом цикле; тогда как строка формата - это литерал, созданный до запуска кода, когда он преобразуется в байт-код. –

+0

@AlexHall Возможно, это связано с тем, что 'x' присваивается локальной переменной при передаче методу' format', но ее нужно найти в 'globals' с помощью' f "..." ' синтаксис. – schwobaseggl

+1

@AlexHall: это не ошибка. Существует просто другая реализация под капотом, так как строка формата должна анализироваться во время компиляции, тогда как 'str.format()' анализирует слоты в * runtime *. –

ответ

23

Примечание: Этот ответ был написан для релизов версии Python 3.6 alpha. A new opcode added to 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) оп-коды этого результата. Новый 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 Release Schedule для таблицы времени, а также Python issue #27078 (открыт в ответ на этот вопрос) для обсуждения того, как еще больше повысить производительность форматированных строковых литералов.

+0

Действительно приятное объяснение, спасибо! Я даже не подозревал, что существует магия метода __format__. –

+0

Почему он расширен до '. '.join ([...])', а не конкатенации строк? –

+3

@AlexHall: поскольку конкатенация строк имеет характеристики производительности O (N^2). A + B + C сначала должен создать строку для A + B, а затем скопировать результат вместе с C в новую строку. –

16

До 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(...) вызов, потому что для последней строки интерпретируется каждый раз, когда строка форматирования.


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

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

0

Просто обновление, отметив, что это похоже на разрешение в выпуске 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 
+4

Да, как уже было сказано в ответе Антти, в котором подробно объясняется, что делает новый код операции. –