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