Почему литеральные форматированные строки так медленны в 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__('')])
. Полученный код был неэффективен по нескольким причинам:
- он построил последовательность фрагментов строки...
- ... и эта последовательность была списком, а не кортежем! (это немного быстрее, чтобы построить кортежи, чем списки).
- он толкнул пустую строку в стеке
- он посмотрел
join
метод на пустой строке - это вызывается
__format__
на даже голых объектах Unicode, для которых__format__('')
всегда вернетсяself
, или целочисленные объекты, для которых__format__('')
как аргумент вернулсяstr(self)
. -
__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