Python: expression du générateur vs rendement

90

En Python, y a-t-il une différence entre la création d'un objet générateur via une expression de générateur et l'utilisation de l' instruction yield ?

En utilisant le rendement :

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

Utilisation de l' expression du générateur :

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

Les deux fonctions renvoient des objets générateurs, qui produisent des tuples, par exemple (0,0), (0,1) etc.

Des avantages de l'un ou de l'autre? Pensées?


Merci à tous! Il y a beaucoup d'informations intéressantes et d'autres références dans ces réponses!

cschol
la source
2
Choisissez celui que vous trouvez le plus lisible.
user238424

Réponses:

74

Il n'y a que de légères différences entre les deux. Vous pouvez utiliser le dismodule pour examiner ce genre de choses par vous-même.

Edit: Ma première version a décompilé l'expression du générateur créée au module-scope dans l'invite interactive. C'est légèrement différent de la version de l'OP car il est utilisé dans une fonction. J'ai modifié cela pour correspondre au cas réel de la question.

Comme vous pouvez le voir ci-dessous, le générateur "yield" (premier cas) a trois instructions supplémentaires dans la configuration, mais du premier, FOR_ITERelles ne diffèrent que sur un seul point: l'approche "yield" utilise un LOAD_FASTà la place d'un LOAD_DEREFà l'intérieur de la boucle. Le LOAD_DEREFest "plutôt plus lent" que LOAD_FAST, donc cela rend la version "yield" légèrement plus rapide que l'expression du générateur pour des valeurs suffisamment grandes de x(la boucle externe) car la valeur de yest chargée légèrement plus rapidement à chaque passage. Pour des valeurs plus petites, xce serait légèrement plus lent en raison de la surcharge supplémentaire du code de configuration.

Il pourrait également être intéressant de souligner que l'expression du générateur serait généralement utilisée en ligne dans le code, plutôt que de l'encapsuler avec la fonction comme ça. Cela supprimerait un peu la surcharge de configuration et garderait l'expression du générateur légèrement plus rapide pour des valeurs de boucle plus petites, même si cela LOAD_FASTdonnait un avantage à la version "yield" autrement.

Dans un cas comme dans l'autre, la différence de performance ne suffirait pas à justifier le choix entre l'un ou l'autre. La lisibilité compte beaucoup plus, utilisez donc celui qui vous semble le plus lisible pour la situation actuelle.

>>> 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
Peter Hansen
la source
Accepté - pour l'explication détaillée de la différence en utilisant dis. Merci!
cschol
J'ai mis à jour pour inclure un lien vers une source qui prétend qu'elle LOAD_DEREFest "plutôt lente", donc si les performances importaient vraiment, un timing réel timeitserait bon. Une analyse théorique ne va pas plus loin.
Peter Hansen
36

Dans cet exemple, pas vraiment. Mais yieldpeut être utilisé pour des constructions plus complexes - par exemple, il peut également accepter des valeurs de l'appelant et modifier le flux en conséquence. Lisez PEP 342 pour plus de détails (c'est une technique intéressante à connaître).

Quoi qu'il en soit, le meilleur conseil est d' utiliser ce qui est le plus clair pour vos besoins .

PS Voici un exemple de coroutine simple 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!")
Eli Bendersky
la source
8
+1 pour la liaison avec David Beazley. Sa présentation sur les coroutines est la chose la plus époustouflante que j'ai lue depuis longtemps. Pas aussi utile, peut-être, que sa présentation sur les générateurs, mais néanmoins incroyable.
Robert Rossney
18

Il n'y a aucune différence pour le type de boucles simples que vous pouvez insérer dans une expression de générateur. Cependant, le rendement peut être utilisé pour créer des générateurs qui effectuent un traitement beaucoup plus complexe. Voici un exemple simple pour générer la séquence 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]
Dave Kirby
la source
5
+1 qui est super cool ... je ne peux pas dire que j'ai jamais vu une implémentation aussi courte et douce sans récursivité.
JudoWill
Extrait de code trompeusement simple - je pense que Fibonacci sera heureux de le voir !!
user-asterix
10

Dans l'utilisation, notez une distinction entre un objet générateur et une fonction générateur.

Un objet générateur est à usage unique, contrairement à une fonction générateur, qui peut être réutilisée chaque fois que vous l'appelez à nouveau, car elle renvoie un nouvel objet générateur.

Les expressions génératrices sont en pratique généralement utilisées «brutes», sans les envelopper dans une fonction, et elles renvoient un objet générateur.

Par exemple:

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()))

qui sort:

[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]

Comparez avec une utilisation légèrement différente:

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

qui sort:

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

Et comparez avec une expression de générateur:

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))

qui produit également:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
Craig McQueen
la source
8

L'utilisation yieldest bien si l'expression est plus compliquée que de simples boucles imbriquées. Entre autres, vous pouvez renvoyer une première ou une dernière valeur spéciale. Considérer:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)
Tor Valamo
la source
5

En pensant aux itérateurs, le itertoolsmodule:

... standardise un ensemble de base d'outils rapides et efficaces en mémoire, utiles seuls ou en combinaison. Ensemble, ils forment une «algèbre itératrice» permettant de construire succinctement et efficacement des outils spécialisés en Python pur.

Pour la performance, pensez itertools.product(*iterables[, repeat])

Produit cartésien des itérables d'entrée.

Équivaut aux boucles for imbriquées dans une expression de générateur. Par exemple, product(A, B)renvoie la même chose 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)]
>>> 
gimel
la source
4

Oui, il y a une différence.

Pour l'expression du générateur (x for var in expr), iter(expr)est appelée lorsque l'expression est créée .

Lors de l'utilisation defet yieldpour créer un générateur, comme dans:

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

g = my_generator()

iter(expr)n'est pas encore appelé. Il ne sera appelé que lors de l'itération g(et peut ne pas être appelé du tout).

Prenant cet itérateur comme exemple:

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

Ce code:

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

tandis que:

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)

Comme la plupart des itérateurs ne font pas beaucoup de choses __iter__, il est facile de rater ce comportement. Un exemple concret serait celui de Django QuerySet, qui récupère les données__iter__ et data = (f(x) for x in qs)peut prendre beaucoup de temps, tandis que def g(): for x in qs: yield f(x)suivi par data=g()reviendrait immédiatement.

Pour plus d'informations et la définition formelle, reportez-vous à PEP 289 - Expressions de générateur .

Udi
la source
0

Il y a une différence qui pourrait être importante dans certains contextes qui n'a pas encore été signalée. L'utilisation yieldvous empêche d'utiliser returnpour autre chose que de déclencher implicitement StopIteration (et les choses liées aux coroutines) .

Cela signifie que ce code est mal formé (et le transmettre à un interprète vous donnera un 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)

D'un autre côté, ce code fonctionne comme un charme:

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)
Adrien
la source