Есть, насколько я знаю, три способа создания генератора через понимание 1.
Классический:
def f1():
g = (i for i in range(10))
Вариант yield
:
def f2():
g = [(yield i) for i in range(10)]
Вариант yield from
(который вызывает SyntaxError
, кроме внутренней функции):
def f3():
g = [(yield from range(10))]
Три варианта приводят к разному байт-коду, что не удивительно. Казалось бы логичным, что первый из них является лучшим, поскольку он представляет собой выделенный, простой синтаксис для создания генератора через понимание. Тем не менее, это не тот, который производит кратчайший байт-код.
Разбор в Python 3.6
Понимание классического генератора
>>> dis.dis(f1)
4 0 LOAD_CONST 1 (<code object <genexpr> at...>)
2 LOAD_CONST 2 ('f1.<locals>.<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
5 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield
вариант
>>> dis.dis(f2)
8 0 LOAD_CONST 1 (<code object <listcomp> at...>)
2 LOAD_CONST 2 ('f2.<locals>.<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
9 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield from
вариант
>>> dis.dis(f3)
12 0 LOAD_GLOBAL 0 (range)
2 LOAD_CONST 1 (10)
4 CALL_FUNCTION 1
6 GET_YIELD_FROM_ITER
8 LOAD_CONST 0 (None)
10 YIELD_FROM
12 BUILD_LIST 1
14 STORE_FAST 0 (g)
13 16 LOAD_FAST 0 (g)
18 RETURN_VALUE
Кроме того, сравнение timeit
показывает, что вариант yield from
является самым быстрым (все еще выполняется с Python 3.6):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
более или менее в 2,7 раза быстрее, чем f1
и f2
.
В качестве Leon, упомянутого в комментарии, эффективность генератора лучше всего измеряется скоростью, с которой он может быть повторен. Поэтому я изменил три функции, чтобы они перебирали генераторы и вызывали фиктивную функцию.
def f():
pass
def fn():
g = ...
for _ in g:
f()
Результаты еще более очевидны:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
f3
теперь в 8,4 раза быстрее, чем f1
, и в 9,3 раза быстрее, чем f2
.
Примечание. Результаты более или менее одинаковы, если итерабельность не range(10)
, а статическая итерация, например [0, 1, 2, 3, 4, 5]
.
Следовательно, разница в скорости не имеет ничего общего с range
как-то оптимизирована.
Итак, каковы различия между тремя способами?
В частности, в чем разница между вариантом yield from
и двумя другими?
Является ли это обычное поведение естественной конструкцией (elt for elt in it)
медленнее, чем сложная [(yield from it)]
?
Должен ли я отныне заменить первый на всех во всех моих сценариях или есть ли какие-либо недостатки в использовании конструкции yield from
?
Изменить
Все это связано, поэтому мне не хочется открывать новый вопрос, но это становится еще более странным.
Я попытался сравнить range(10)
и [(yield from range(10))]
.
def f1():
for i in range(10):
print(i)
def f2():
for i in [(yield from range(10))]:
print(i)
>>> timeit(f1, number=100000)
26.715589237537195
>>> timeit(f2, number=100000)
0.019948781941049987
Итак. Теперь, итерация по [(yield from range(10))]
в 186 раз быстрее, чем итерация по голой range(10)
?
Как вы объясните, почему итерация по [(yield from range(10))]
выполняется намного быстрее, чем повторение с помощью range(10)
?
1: для скептического ответа следующие три выражения вызывают объект generator
; попробуйте и назовите type
на них.