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!
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
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!")
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]
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]
[]
[]
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)
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)]
>>>
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 .
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)