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.