Python: expression du générateur en fonction du rendement

en Python, y a-t-il une différence entre la création d'un objet générateur à travers l'expression generator versus l'utilisation de la déclaration yield ?

utilisant rendement :

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

utilisant expression génératrice :

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

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


merci à tous! Il y a beaucoup de bonnes informations et d'autres références dans ces réponses!

77
demandé sur cschol 2010-01-03 19:09:26

8 réponses

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 pour vous-même.

Edit: ma première version décompose 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 avec elle utilisée à l'intérieur d'une fonction. J'ai modifié ceci 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 ils diffèrent à un seul égard: l'approche" rendement "utilise un LOAD_FAST à la place d'un LOAD_DEREF à l'intérieur de la boucle. Le LOAD_DEREF est "plutôt lent " que LOAD_FAST , donc il rend la version "rendement" légèrement plus rapide que l'expression de générateur pour des valeurs assez grandes de x (la boucle extérieure) parce que la valeur de y est chargement légèrement plus vite à 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 pourrait également être intéressant de souligner que l'expression generator serait habituellement utilisée en ligne dans le code, plutôt que de l'envelopper avec la fonction comme cela. Cela supprimerait un peu de la mise en place et maintiendrait l'expression du générateur légèrement plus rapide pour des valeurs de boucle plus petites, même si LOAD_FAST donnait "yield" version un avantage sinon.

dans aucun des deux cas, la différence de rendement ne serait suffisante pour justifier une décision entre l'un et l'autre. La lisibilité compte beaucoup plus, donc utilisez celui qui vous semble le plus lisible pour la situation présente.

>>> 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
répondu Peter Hansen 2010-01-03 18:22:10

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

de toute façon, le meilleur conseil est utiliser tout ce qui est plus clair pour vos besoins .

voici un simple exemple 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 __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!")
35
répondu Eli Bendersky 2010-01-03 16:18:36

il n'y a pas de différence pour le genre 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 font 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
répondu Dave Kirby 2010-01-03 16:30:39

utiliser yield est agréable si l'expression est plus compliquée que de simples boucles imbriquées. Entre autres choses, vous pouvez retourner une première ou une dernière valeur spéciale. Prendre en considération:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)
8
répondu Tor Valamo 2010-01-03 16:13:13

dans l'usage, noter une distinction entre un objet générateur vs une fonction génératrice.

un objet générateur est utilisé une seule fois, contrairement à une fonction génératrice, qui peut être réutilisée à chaque fois que vous l'appelez de nouveau, parce qu'il renvoie un objet générateur neuf.

Générateur d'expressions sont généralement utilisés "brut", sans les envelopper dans une fonction, et ils retournent un objet de 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]

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

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

et comparer 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 aussi:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
8
répondu Craig McQueen 2010-01-12 06:45:23

en pensant aux itérateurs, le module itertools :

... standardise un ensemble Central d'outils rapides et économes en mémoire qui sont utiles par eux-mêmes ou en combinaison. Ensemble, ils forment une "algèbre itératrice" permettant de construire des outils spécialisés de façon succincte et efficace en Python pur.

pour la performance, considérer itertools.product(*iterables[, repeat])

produit cartésien d'entrées itérables.

équivalent à des boucles 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)]
>>> 
5
répondu gimel 2010-01-03 16:21:44

Oui il y a une différence.

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

Lorsqu'on utilise def et yield pour 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 sera appelé uniquement lors de l'itération sur g (et pourrait ne pas être appelé à 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)

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

Puisque la plupart des itérateurs ne fais pas beaucoup de choses dans __iter__ , il est facile de manquer ce comportement. Un exemple du monde réel serait QuerySet de Django, qui "fetch data in __iter__ et data = (f(x) for x in qs) pourrait prendre beaucoup de temps, tandis que def g(): for x in qs: yield f(x) suivi de data=g() je reviendrai immédiatement.

pour plus d'information et la définition formelle, se référer à PEP 289 -- Generator Expressions .

2
répondu Udi 2017-05-17 23:10:21

il y a une différence qui pourrait être importante dans certains contextes qui n'a pas encore été soulignée. L'utilisation de yield vous empêche d'utiliser return pour quelque chose d'autre que augmentant implicitement L'arrêt de la ventilation (et coroutines connexes) .

cela signifie que ce code est mal formé (et le donner à 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'autre part, 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)
0
répondu Adrien 2017-05-23 11:47:19