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 quef1e 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?

Editar

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.

questionAnswers(3)

yourAnswerToTheQuestion