Retorno ou rendimento de uma função que chama um gerador?

30

Eu tenho um gerador generatore também um método de conveniência para ele - generate_all.

def generator(some_list):
  for i in some_list:
    yield do_something(i)

def generate_all():
  some_list = get_the_list()
  return generator(some_list) # <-- Is this supposed to be return or yield?

Deveria generate_all returnou yield? Eu quero que os usuários dos dois métodos usem o mesmo, ou seja,

for x in generate_all()

deve ser igual a

some_list = get_the_list()
for x in generate(some_list)
hyankov
fonte
2
Há uma razão para usar qualquer um. Neste exemplo, o retorno é mais eficiente
Mad Physicist
11
Isso me lembra uma pergunta semelhante que fiz uma vez: "yield from iterable" vs "return iter (iterable)" . Embora não seja especificamente sobre geradores, é basicamente o mesmo que geradores e iteradores são bastante semelhantes em python. Também a estratégia de comparar o bytecode como proposto pela resposta pode ser útil aqui.
PeterE

Respostas:

12

Os geradores estão avaliando preguiçosamente returnou yieldse comportarão de maneira diferente quando você estiver depurando seu código ou se uma exceção for lançada.

Com returnqualquer exceção que você generatornão saiba generate_all, é porque quando generatoré realmente executado você já deixou a generate_allfunção. Com yieldlá vai ter generate_allno traceback.

def generator(some_list):
    for i in some_list:
        raise Exception('exception happened :-)')
        yield i

def generate_all():
    some_list = [1,2,3]
    return generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-3-b19085eab3e1> in <module>
      8     return generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-3-b19085eab3e1> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

E se estiver usando yield from:

def generate_all():
    some_list = [1,2,3]
    yield from generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-4-be322887df35> in <module>
      8     yield from generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-4-be322887df35> in generate_all()
      6 def generate_all():
      7     some_list = [1,2,3]
----> 8     yield from generator(some_list)
      9 
     10 for item in generate_all():

<ipython-input-4-be322887df35> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

No entanto, isso tem um custo de desempenho. A camada adicional do gerador possui alguma sobrecarga. Então return, geralmente será um pouco mais rápido que yield from ...(ou for item in ...: yield item). Na maioria dos casos, isso não importa muito, porque o que você faz no gerador normalmente domina o tempo de execução, para que a camada adicional não seja perceptível.

No entanto, yieldpossui algumas vantagens adicionais: você não está restrito a uma única iterável, mas também pode gerar itens adicionais com facilidade:

def generator(some_list):
    for i in some_list:
        yield i

def generate_all():
    some_list = [1,2,3]
    yield 'start'
    yield from generator(some_list)
    yield 'end'

for item in generate_all():
    print(item)
start
1
2
3
end

No seu caso, as operações são bastante simples e não sei se é necessário criar várias funções para isso, seria possível usar facilmente a mapexpressão incorporada ou de gerador:

map(do_something, get_the_list())          # map
(do_something(i) for i in get_the_list())  # generator expression

Ambos devem ser idênticos (exceto por algumas diferenças quando ocorrerem exceções) a serem usados. E se eles precisarem de um nome mais descritivo, você ainda poderá agrupá-los em uma função.

Existem vários auxiliares que envolvem operações muito comuns nos iterables internos e outros podem ser encontrados no itertoolsmódulo interno. Em casos tão simples, eu simplesmente recorreria a esses e somente para casos não triviais escreva seus próprios geradores.

Mas suponho que seu código real seja mais complicado, pelo que pode não ser aplicável, mas achei que não seria uma resposta completa sem mencionar alternativas.

MSeifert
fonte
17

Você provavelmente está procurando Delegação de Gerador (PEP380)

Para iteradores simples, yield from iterableé essencialmente apenas uma forma abreviada defor item in iterable: yield item

def generator(iterable):
  for i in iterable:
    yield do_something(i)

def generate_all():
  yield from generator(get_the_list())

É bastante conciso e também tem várias outras vantagens, como poder encadear iteráveis ​​arbitrários / diferentes!

ti7
fonte
Oh você quer dizer o nome de list? É um mau exemplo, não um código real colado na pergunta, eu provavelmente deveria editá-lo.
hyankov 5/01
Sim - não tenha medo, sou bastante culpado de exemplo de código que nem será executado na primeira pergunta.
ti7
2
O primeiro também pode ser uma linha :). yield from map(do_something, iterable)ou até mesmoyield from (do_something(x) for x in iterable)
Mad Physicist
2
"É um exemplo de código todo o caminho!"
ti7 5/01
3
Você só precisa de delegação se estiver fazendo algo diferente de apenas retornar o novo gerador. Se você apenas devolver o novo gerador, nenhuma delegação será necessária. Portanto, yield fromé inútil, a menos que seu invólucro faça algo mais gerador-y.
ShadowRanger 5/01
14

return generator(list)faz o que você quer. Mas note que

yield from generator(list)

seria equivalente, mas com a oportunidade de gerar mais valores após o generatoresgotamento. Por exemplo:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
fonte
5
Eu acredito que há uma diferença sutil entre yield frome returnquando o consumidor do gerador é throwsuma exceção dentro dele - e com outras operações que são influenciadas pelo rastreamento da pilha.
WorldSEnder 5/01
9

As duas instruções a seguir parecerão funcionalmente equivalentes nesse caso específico:

return generator(list)

e

yield from generator(list)

O posterior é aproximadamente o mesmo que

for i in generator(list):
    yield i

A returninstrução retorna o gerador que você está procurando. Uma instrução yield fromou yieldtransforma toda a sua função em algo que retorna um gerador, que passa pelo que você está procurando.

Do ponto de vista do usuário, não há diferença. Internamente, no entanto, returné indiscutivelmente mais eficiente, pois não envolve generator(list)um gerador de passagem supérfluo. Se você planeja fazer qualquer processamento nos elementos do gerador empacotado, use alguma forma, é yieldclaro.

Físico louco
fonte
4

Você faria returnisso.

yielding * causaria generate_all()a avaliação de um gerador em si, e chamar nextesse gerador externo retornaria o gerador interno retornado pela primeira função, que não é o que você deseja.

* Não incluindo yield from

Carcinigenicado
fonte