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