web-dev-qa-db-fra.com

Python: expression du générateur vs rendement

En Python, existe-t-il une différence entre la création d'un objet générateur avec un générateur d'expression et l'utilisation de rendement ?

En utilisant yield:

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

Utilisation de générateur d'expression:

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 n-uplets, par ex. (0,0), (0,1) etc.

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


Merci à tous! Il y a beaucoup de bonnes informations et de références supplémentaires dans ces réponses!

78
cschol

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

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

Comme vous pouvez le voir ci-dessous, le générateur "de rendement" (premier cas) a trois instructions supplémentaires dans la configuration, mais à partir du premier FOR_ITER, elles ne diffèrent que par un point: l'approche "de rendement" utilise un LOAD_FAST au lieu d'un LOAD_DEREF à l'intérieur du boucle. Le LOAD_DEREF est "plutôt plus lent" 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 extérieure) car la valeur de y est chargée légèrement plus rapidement à chaque fois. passer. Pour des valeurs plus faibles de x, il serait légèrement plus lent en raison de la charge supplémentaire du code d'installation.

Il peut également être intéressant de noter que l'expression du générateur serait généralement utilisée inline dans le code, plutôt que de l'encapsuler avec la fonction. Cela supprime un peu la surcharge liée à la configuration et maintient l'expression du générateur un peu plus rapide pour les valeurs de boucle plus petites, même si LOAD_FAST donne un avantage à la version "rendement".

Dans aucun des deux cas, la différence de performance ne suffirait à justifier la décision entre l’un ou l’autre. La lisibilité compte beaucoup plus, utilisez donc celui qui vous semble le plus lisible pour la situation. 

>>> 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
68
Peter Hansen

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

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

P.S. Voici un exemple simple de coroutine de Dave Beazley :

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __== '__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!")
35
Eli Bendersky

Il n'y a pas de différence pour le type de boucles simples que vous pouvez insérer dans une expression génératrice. 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 1:
...        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]
17
Dave Kirby

En utilisation, notez une distinction entre un objet générateur et une fonction génératrice.

Un objet générateur est à usage unique, contrairement à une fonction générateur, qui peut être réutilisée à chaque appel, car il renvoie un nouvel objet générateur.

Les expressions de générateur sont en pratique généralement utilisées "raw", 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 produit:

[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 produit:

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

Et comparons 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]
[]
[]
8
Craig McQueen

Utiliser yield est agréable si l'expression est plus compliquée que les boucles imbriquées. Vous pouvez entre autres 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)
8
Tor Valamo

En ce qui concerne les itérateurs, le module itertools:

... standardise un ensemble d'outils de base rapides et efficaces en termes de mémoire, utiles par eux-mêmes ou en combinaison. Ensemble, ils forment une "algèbre itérative" permettant de construire des outils spécialisés de manière succincte et efficace en Python pur.

Pour des performances, considérez itertools.product(*iterables[, repeat])

Produit cartésien d'entrée iterables.

Équivalent aux boucles for imbriquées dans une expression génératrice. 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)]
>>> 
5
gimel

Oui, il y a une différence.

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

Lorsque vous utilisez def et yield pour créer un générateur, procédez comme suit:

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

g = my_generator()

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

En 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 dans __iter__, il est facile de rater ce comportement. La variable QuerySet de Django, qui extraire des données dans __iter__ et data = (f(x) for x in qs), peut prendre beaucoup de temps, par exemple, alors que def g(): for x in qs: yield f(x) suivi de data=g() retournera immédiatement.

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

2
Udi

Il y a une différence qui pourrait être importante dans certains contextes et qui n'a pas encore été soulignée. L'utilisation de yield vous empêche d'utiliser return pour autre chose que soulève implicitement StopIteration (et les éléments associés aux coroutines) .

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

Par contre, ce code fonctionne à merveille:

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