Python: expressão do gerador vs. rendimento


90

Em Python, há alguma diferença entre criar um objeto gerador por meio de uma expressão geradora e usar a instrução yield ?

Usando o rendimento :

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

Usando expressão geradora :

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

Ambas as funções retornam objetos geradores, que produzem tuplas, por exemplo (0,0), (0,1) etc.

Alguma vantagem de um ou de outro? Pensamentos?


Obrigado a todos! Há muitas informações úteis e referências adicionais nessas respostas!


2
Escolha aquele que você achar mais legível.
user238424

Respostas:


74

Existem apenas pequenas diferenças entre os dois. Você pode usar o dismódulo para examinar esse tipo de coisa por si mesmo.

Edit: Minha primeira versão descompilou a expressão geradora criada no escopo do módulo no prompt interativo. Isso é um pouco diferente da versão do OP com ele usado dentro de uma função. Eu modifiquei isso para corresponder ao caso real na questão.

Como você pode ver abaixo, o gerador de "rendimento" (primeiro caso) tem três instruções extras na configuração, mas desde o primeiro FOR_ITERelas diferem em apenas um aspecto: a abordagem de "rendimento" usa um LOAD_FASTno lugar de um LOAD_DEREFdentro do loop. O LOAD_DEREFé "bastante mais lento" do que LOAD_FAST, portanto, torna a versão de "rendimento" um pouco mais rápida do que a expressão geradora para valores grandes o suficiente de x(o loop externo) porque o valor de yé carregado um pouco mais rápido a cada passagem. Para valores menores x, seria um pouco mais lento por causa da sobrecarga extra do código de configuração.

Também pode valer a pena apontar que a expressão geradora normalmente seria usada embutida no código, em vez de envolvê-la com a função dessa forma. Isso removeria um pouco da sobrecarga de configuração e manteria a expressão do gerador um pouco mais rápida para valores de loop menores, mesmo que LOAD_FASTdesse à versão "rendimento" uma vantagem de outra forma.

Em nenhum dos casos, a diferença de desempenho seria suficiente para justificar a decisão entre um ou outro. A legibilidade conta muito mais, portanto, use o que parecer mais legível para a situação em questão.

>>> def Generator(x, y):
...     for i in xrange(x):
...         for j in xrange(y):
...             yield(i, j)
...
>>> dis.dis(Generator)
  2           0 SETUP_LOOP              54 (to 57)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_FAST                0 (x)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                40 (to 56)
             16 STORE_FAST               2 (i)

  3          19 SETUP_LOOP              31 (to 53)
             22 LOAD_GLOBAL              0 (xrange)
             25 LOAD_FAST                1 (y)
             28 CALL_FUNCTION            1
             31 GET_ITER
        >>   32 FOR_ITER                17 (to 52)
             35 STORE_FAST               3 (j)

  4          38 LOAD_FAST                2 (i)
             41 LOAD_FAST                3 (j)
             44 BUILD_TUPLE              2
             47 YIELD_VALUE
             48 POP_TOP
             49 JUMP_ABSOLUTE           32
        >>   52 POP_BLOCK
        >>   53 JUMP_ABSOLUTE           13
        >>   56 POP_BLOCK
        >>   57 LOAD_CONST               0 (None)
             60 RETURN_VALUE
>>> def Generator_expr(x, y):
...    return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
  2           0 SETUP_LOOP              47 (to 50)
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                40 (to 49)
              9 STORE_FAST               1 (i)
             12 SETUP_LOOP              31 (to 46)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_DEREF               0 (y)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                17 (to 45)
             28 STORE_FAST               2 (j)
             31 LOAD_FAST                1 (i)
             34 LOAD_FAST                2 (j)
             37 BUILD_TUPLE              2
             40 YIELD_VALUE
             41 POP_TOP
             42 JUMP_ABSOLUTE           25
        >>   45 POP_BLOCK
        >>   46 JUMP_ABSOLUTE            6
        >>   49 POP_BLOCK
        >>   50 LOAD_CONST               0 (None)
             53 RETURN_VALUE

Aceito - para a explicação detalhada da diferença usando dis. Obrigado!
cschol

Eu atualizei para incluir um link para uma fonte que afirma LOAD_DEREFser "um pouco mais lento", portanto, se o desempenho realmente importasse, algum tempo real timeitseria bom. Uma análise teórica vai apenas até certo ponto.
Peter Hansen

36

Neste exemplo, não realmente. Mas yieldpode ser usado para construções mais complexas - por exemplo , pode aceitar valores do chamador também e modificar o fluxo como resultado. Leia PEP 342 para mais detalhes (é uma técnica interessante que vale a pena conhecer).

De qualquer forma, o melhor conselho é usar o que for mais claro para suas necessidades .

PS Aqui está um exemplo simples de co-rotina de Dave Beazley :

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")

8
+1 por vincular a David Beazley. Sua apresentação sobre corrotinas é a coisa mais alucinante que li em muito tempo. Não tão útil, talvez, quanto sua apresentação sobre geradores, mas mesmo assim incrível.
Robert Rossney

18

Não há diferença para o tipo de loops simples que você pode inserir em uma expressão de gerador. No entanto, o rendimento pode ser usado para criar geradores que fazem um processamento muito mais complexo. Aqui está um exemplo simples para gerar a sequência de fibonacci:

>>> def fibgen():
...    a = b = 1
...    while True:
...        yield a
...        a, b = b, a+b

>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

5
+1 que é super legal ... não posso dizer que já vi uma implementação de mentira tão curta e doce sem recursão.
JudoWill

Trecho de código enganosamente simples - acho que Fibonacci ficará feliz em vê-lo !!
user-asterix

10

No uso, observe uma distinção entre um objeto gerador e uma função geradora.

Um objeto gerador é usado apenas uma vez, em contraste com uma função de gerador, que pode ser reutilizada cada vez que você chamá-lo novamente, porque retorna um novo objeto gerador.

As expressões geradoras são, na prática, normalmente usadas "brutas", sem envolvê-las em uma função, e retornam um objeto gerador.

Por exemplo:

def range_10_gen_func():
    x = 0
    while x < 10:
        yield x
        x = x + 1

print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))

que produz:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Compare com um uso ligeiramente diferente:

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

que produz:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

E compare com uma expressão geradora:

range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))

que também produz:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

8

Usar yieldé bom se a expressão for mais complicada do que apenas loops aninhados. Entre outras coisas, você pode retornar um primeiro valor especial ou um último valor especial. Considerar:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)

5

Ao pensar em iteradores, o itertoolsmódulo:

... padroniza um conjunto básico de ferramentas rápidas e eficientes em termos de memória que são úteis isoladamente ou em combinação. Juntos, eles formam uma “álgebra iterativa”, tornando possível construir ferramentas especializadas de forma sucinta e eficiente em Python puro.

Para desempenho, considere itertools.product(*iterables[, repeat])

Produto cartesiano de iteráveis ​​de entrada.

Equivalente a loops for aninhados em uma expressão geradora. Por exemplo, product(A, B)retorna o mesmo que ((x,y) for x in A for y in B).

>>> import itertools
>>> def gen(x,y):
...     return itertools.product(xrange(x),xrange(y))
... 
>>> [t for t in gen(3,2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>> 

4

Sim, há uma diferença.

Para a expressão geradora (x for var in expr), iter(expr)é chamado quando a expressão é criada .

Ao usar defe yieldcriar um gerador, como em:

def my_generator():
    for var in expr:
        yield x

g = my_generator()

iter(expr)ainda não foi chamado. Ele será chamado apenas durante a iteração g(e pode nem ser chamado).

Tomando este iterador como exemplo:

from __future__ import print_function


class CountDown(object):
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        print("ITER")
        return self

    def __next__(self):
        if self.n == 0:
            raise StopIteration()
        self.n -= 1
        return self.n

    next = __next__  # for python2

Este código:

g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
print("Go!")
for x in g1:
    print(x)

enquanto:

def my_generator():
    for i in CountDown(3):
        yield i ** 2


g2 = my_generator()
print("Go!")
for x in g2:  # "ITER" is only printed here
    print(x)

Como a maioria dos iteradores não faz muitas coisas __iter__, é fácil não perceber esse comportamento. Um exemplo do mundo real seria o do Django QuerySet, que busca dados__iter__ e data = (f(x) for x in qs)pode levar muito tempo, enquanto def g(): for x in qs: yield f(x)seguido por data=g()retornaria imediatamente.

Para obter mais informações e a definição formal, consulte PEP 289 - Expressões do gerador .


0

Há uma diferença que pode ser importante em alguns contextos que ainda não foi apontada. Usar yieldimpede que você use returnpara outra coisa que não gerar StopIteration implicitamente (e coisas relacionadas a corrotinas) .

Isso significa que este código está mal formado (e alimentá-lo a um intérprete fornecerá um AttributeError):

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
            yield item

print(mary_poppins_purse(True).temperature)

Por outro lado, este código funciona perfeitamente:

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        return (item for item in ['lamp', 'mirror', 'coat rack',
                                  'tape measure', 'ficus'])

print(mary_poppins_purse(True).temperature)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.