Diferenças entre expressões de compreensão do gerador
Até onde eu sei, existem três maneiras de criar um gerador através de uma compreensão1.
O clássico:
def f1():
g = (i for i in range(10))
oyield
variante:
def f2():
g = [(yield i) for i in range(10)]
oyield from
variante (que gera umaSyntaxError
exceto dentro de uma função):
def f3():
g = [(yield from range(10))]
As três variantes levam a diferentes códigos de bytes, o que não é realmente surpreendente. Parece lógico que o primeiro seja o melhor, já que é uma sintaxe direta e dedicada a criar um gerador através da compreensão. No entanto, não é o que produz o menor bytecode.
Desmontado em Python 3.6
Compreensão clássica do gerador
>>> 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
variante
>>> 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
variante
>>> 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
Além disso, umtimeit
comparação mostra que oyield from
A variante é a mais rápida (ainda é executada no Python 3.6):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
é mais ou menos 2,7 vezes mais rápido quef1
ef2
.
ComoLeon Como mencionado em um comentário, a eficiência de um gerador é melhor medida pela velocidade em que ele pode ser iterado. Então, mudei as três funções para que elas iterassem nos geradores e chamassem uma função fictícia.
def f():
pass
def fn():
g = ...
for _ in g:
f()
Os resultados são ainda mais flagrantes:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
f3
agora é 8,4 vezes mais rápido quef1
e 9,3 vezes mais rápido quef2
.
Nota: Os resultados são mais ou menos os mesmos quando o iterável não érange(10)
mas uma iterável estática, como[0, 1, 2, 3, 4, 5]
. Portanto, a diferença de velocidade não tem nada a ver comrange
sendo de alguma forma otimizado.
Então, quais são as diferenças entre as três maneiras? Mais especificamente, qual é a diferença entre oyield from
variante e os outros dois?
É este comportamento normal que o construto natural(elt for elt in it)
é mais lento que o complicado[(yield from it)]
? De agora em diante, substituirei o primeiro pelo último em todos os meus scripts ou há algum inconveniente em usar oyield from
construir?
Tudo isso está relacionado, por isso não tenho vontade de abrir uma nova pergunta, mas isso está ficando ainda mais estranho. Eu tentei compararrange(10)
e[(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
Assim. Agora, iterando sobre[(yield from range(10))]
é 186 vezes mais rápido do que iterandorange(10)
?
Como você explica por que iterar sobre[(yield from range(10))]
é muito mais rápido do que repetindorange(10)
?
1: Para os céticos, as três expressões a seguir produzem umagenerator
objeto; tente ligartype
neles.