Retour ou rendement d'une fonction qui appelle un générateur?

30

J'ai un générateur generatoret aussi une méthode pratique - 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?

Devrait generate_all returnou yield? Je veux que les utilisateurs des deux méthodes l'utilisent de la même manière, c'est-à-dire

for x in generate_all()

devrait être égal à

some_list = get_the_list()
for x in generate(some_list)
hyankov
la source
2
Il y a une raison d'utiliser non plus. Pour cet exemple, le retour est plus efficace
Mad Physicist
1
Cela me rappelle une question similaire que j'ai posée une fois: «rendement de l'itérable» vs «retour de l'itére (itérable)» . Bien qu'il ne s'agisse pas spécifiquement des générateurs, il est fondamentalement identique aux générateurs et les itérateurs sont assez similaires en python. La stratégie de comparaison du bytecode proposée par la réponse peut également être utile ici.
PeterE

Réponses:

12

Les générateurs effectuent une évaluation paresseuse, returnou yieldse comportent différemment lorsque vous déboguez votre code ou si une exception est levée.

Avec returntoute exception qui se produit dans votre generatorne saurez rien generate_all, c'est parce que quand il generatorest vraiment exécuté, vous avez déjà quitté la generate_allfonction. Avec yielddedans ça aura generate_alldans le 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 :-)

Et s'il utilise 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 :-)

Cependant, cela se fait au détriment des performances. La couche de générateur supplémentaire a une surcharge. Ce returnsera donc généralement un peu plus rapide que yield from ...(ou for item in ...: yield item). Dans la plupart des cas, cela n'a pas beaucoup d'importance, car tout ce que vous faites dans le générateur domine généralement le temps d'exécution de sorte que la couche supplémentaire ne soit pas perceptible.

Cela yieldprésente cependant quelques avantages supplémentaires: vous n'êtes pas limité à un seul itérable, vous pouvez également facilement générer des éléments supplémentaires:

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

Dans votre cas, les opérations sont assez simples et je ne sais pas s'il est même nécessaire de créer plusieurs fonctions pour cela, on pourrait facilement utiliser simplement l' mapexpression intégrée ou un générateur à la place:

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

Les deux doivent être identiques (à l'exception de certaines différences lorsque des exceptions se produisent) à utiliser. Et s'ils ont besoin d'un nom plus descriptif, vous pouvez toujours les envelopper dans une seule fonction.

Il existe plusieurs assistants qui encapsulent des opérations très courantes sur les itérables intégrés et d'autres peuvent être trouvés dans le itertoolsmodule intégré. Dans de tels cas simples, je recourrais simplement à ces derniers et seulement pour les cas non triviaux, écrivez vos propres générateurs.

Mais je suppose que votre vrai code est plus compliqué, ce qui peut ne pas être applicable, mais je pensais que ce ne serait pas une réponse complète sans mentionner des alternatives.

MSeifert
la source
17

Vous recherchez probablement une délégation de générateur (PEP380)

Pour les itérateurs simples, il yield from iterables’agit essentiellement d’une forme abrégée 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())

C'est assez concis et a également un certain nombre d'autres avantages, comme la possibilité de chaîner des itérables arbitraires / différents!

ti7
la source
Oh tu veux dire le nom de list? C'est un mauvais exemple, pas du vrai code collé dans la question, je devrais probablement le modifier.
hyankov
Ouais - n'ayez crainte, je suis tout à fait coupable d'un exemple de code qui ne fonctionnera même pas au premier abord.
ti7
2
Le premier peut aussi être un aller simple :). yield from map(do_something, iterable)ou mêmeyield from (do_something(x) for x in iterable)
Mad Physicist
2
"C'est un exemple de code tout en bas!"
ti7
3
Vous n'avez besoin de délégation que si vous faites vous-même autre chose que le simple retour du nouveau générateur. Si vous venez de renvoyer le nouveau générateur, aucune délégation n'est nécessaire. Il yield fromest donc inutile, sauf si votre wrapper fait autre chose générateur-y.
ShadowRanger
14

return generator(list)fait ce que vous voulez. Mais notez que

yield from generator(list)

serait équivalent, mais avec la possibilité de produire plus de valeurs après avoir generatorépuisé. Par exemple:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
la source
5
Je crois qu'il y a une différence subtile entre yield fromet returnlorsque le consommateur du générateur a throwsune exception à l'intérieur de celui-ci - et avec d'autres opérations qui sont influencées par la trace de la pile.
WorldSEnder
9

Les deux instructions suivantes semblent être fonctionnellement équivalentes dans ce cas particulier:

return generator(list)

et

yield from generator(list)

Ce dernier est à peu près le même que

for i in generator(list):
    yield i

L' returninstruction renvoie le générateur que vous recherchez. Une instruction yield fromor yieldtransforme l'ensemble de votre fonction en quelque chose qui renvoie un générateur, qui passe par celui que vous recherchez.

D'un point de vue utilisateur, il n'y a pas de différence. En interne, cependant, le returnest sans doute plus efficace car il ne s'enroule pas generator(list)dans un générateur pass-thru superflu. Si vous prévoyez de faire un traitement sur les éléments du générateur encapsulé, utilisez yieldbien sûr une certaine forme .

Physicien fou
la source
4

Vous le feriez return.

yielding * entraînerait l' generate_all()évaluation d'un générateur lui-même, et invoquer nextce générateur externe retournerait le générateur interne renvoyé par la première fonction, ce qui n'est pas ce que vous voudriez.

* Non compris yield from

Carcigenicate
la source