96 votes

Python : expression de générateur vs. yield

En Python, y a-t-il une différence entre la création d'un objet générateur par l'intermédiaire d'un fichier expression de générateur par rapport à l'utilisation du rendement déclaration ?

Utilisation de rendement :

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

Utilisation de expression de 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.

Y a-t-il des avantages à l'un ou à l'autre ? Vous avez des idées ?

2 votes

Choisissez celui qui vous semble le plus lisible.

77voto

Peter Hansen Points 8487

Il n'y a que de légères différences entre les deux. Vous pouvez utiliser le dis module 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 à module-scope dans l'invite interactive. C'est légèrement différent de la version de l'OP qui l'utilise à l'intérieur d'une fonction. J'ai modifié cette version pour qu'elle corresponde 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 de la première FOR_ITER elles ne diffèrent qu'à un seul égard : l'approche "rendement" utilise une LOAD_FAST à la place d'un LOAD_DEREF à l'intérieur de la boucle. Le site LOAD_DEREF es "plutôt plus lentement" que LOAD_FAST La version "rendement" est donc légèrement plus rapide que l'expression du générateur pour des valeurs suffisamment grandes de x (la boucle externe) parce que la valeur de y est chargé un peu plus rapidement à chaque passage. Pour des valeurs plus petites de x il serait légèrement plus lent en raison de la surcharge du code de configuration.

Il peut également être utile de souligner que l'expression du générateur est généralement utilisée en ligne dans le code, plutôt que de l'intégrer à la fonction comme cela. Cela supprimerait une partie de la surcharge de configuration et permettrait à l'expression du générateur d'être un peu plus rapide pour les petites valeurs de boucle même si LOAD_FAST a donné à la version "rendement" un avantage autrement.

Dans les deux cas, la différence de performance ne serait pas suffisante pour justifier le choix de l'un ou l'autre. La lisibilité compte bien plus, alors utilisez celle qui semble la plus lisible pour la situation en question.

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

1 votes

Accepté - pour l'explication détaillée de la différence en utilisant dis. Merci !

1 votes

J'ai mis à jour pour inclure un lien vers une source qui affirme que LOAD_DEREF est "plutôt plus lent", donc si les performances sont vraiment importantes, un véritable timing avec timeit serait bien. Une analyse théorique ne va pas plus loin.

37voto

Eli Bendersky Points 82298

Dans cet exemple, pas vraiment. Mais yield peut ê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. Lire PEP 342 pour plus de détails (c'est une technique intéressante qui mérite d'être connue).

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

P.S. Voici un exemple simple de coroutine à partir 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 votes

+1 pour le lien vers 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.

19voto

Dave Kirby Points 12310

Il n'y a pas de 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 des traitements beaucoup plus complexes. 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]

5 votes

+1 c'est super cool ... je ne peux pas dire que j'ai déjà vu une implémentation de fibres aussi courte et douce sans récursion.

0 votes

Un extrait de code d'une simplicité déconcertante - je pense que Fibonacci sera heureux de le voir !

11voto

Craig McQueen Points 13194

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

Un objet générateur n'est utilisable qu'une seule fois, contrairement à une fonction générateur, qui peut être réutilisée à chaque fois que vous la rappelez, car elle renvoie un objet générateur neuf.

Dans la pratique, les expressions de générateur sont 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 un usage légèrement différent :

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 l'expression d'un 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 des résultats :

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

8voto

Tor Valamo Points 14209

Utilisation de yield est utile si l'expression est plus compliquée que de simples boucles imbriquées. Entre autres choses, vous pouvez renvoyer une première ou une dernière valeur spéciale. Pensez-y :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X