Is er in Python een verschil tussen het maken van een generatorobject via een generatorexpressieen het gebruik van de yield-instructie?
rendementgebruiken:
def Generator(x, y):
for i in xrange(x):
for j in xrange(y):
yield(i, j)
Gebruik generatoruitdrukking:
def Generator(x, y):
return ((i, j) for i in xrange(x) for j in xrange(y))
Beide functies retourneren generatorobjecten, die tuples produceren, b.v. (0,0), (0,1) enz.
Enige voordelen van het een of het ander? Gedachten?
Antwoord 1, autoriteit 100%
Er zijn slechts kleine verschillen tussen de twee. U kunt de module dis
gebruiken om dit soort dingen zelf te onderzoeken.
Bewerken:mijn eerste versie decompileerde de generator-expressie die was gemaakt op module-scope in de interactieve prompt. Dat is iets anders dan de OP-versie waarin het in een functie wordt gebruikt. Ik heb dit aangepast zodat het overeenkomt met het werkelijke geval in de vraag.
Zoals je hieronder kunt zien, heeft de “yield”-generator (eerste geval) drie extra instructies in de setup, maar van de eerste FOR_ITER
verschillen ze in slechts één opzicht: de “yield”-benadering gebruikt een LOAD_FAST
in plaats van een LOAD_DEREF
in de lus. De LOAD_DEREF
is “vrij langzamer”dan LOAD_FAST
, dus het maakt de “yield”-versie iets sneller dan de generatorexpressie voor waarden die groot genoeg zijn voor x
(de buitenste lus) omdat de waarde van y
wordt bij elke pas iets sneller geladen. Voor kleinere waarden van x
zou het iets langzamer zijn vanwege de extra overhead van de setup-code.
Het is misschien ook de moeite waard om erop te wijzen dat de generator-expressie meestal inline in de code wordt gebruikt, in plaats van deze zo met de functie in te pakken. Dat zou een beetje van de setup-overhead wegnemen en de generator-expressie iets sneller houden voor kleinere luswaarden, zelfs als LOAD_FAST
de “yield”-versie anders een voordeel zou geven.
In geen van beide gevallen zou het prestatieverschil voldoende zijn om een keuze tussen het een of het ander te rechtvaardigen. De leesbaarheid telt veel meer, dus gebruik wat het meest leesbaar is voor de betreffende situatie.
>>> 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
Antwoord 2, autoriteit 48%
In dit voorbeeld niet echt. Maar yield
kan worden gebruikt voor complexere constructies – bijvoorbeeldhet kan ook waarden van de beller accepteren en de stroom als resultaat wijzigen. Lees PEP 342voor meer details (het is een interessante techniek die het waard is om te weten).
Hoe dan ook, het beste advies is gebruik wat voor u duidelijker is.
P.S. Hier is een eenvoudig coroutinevoorbeeld van 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!")
Antwoord 3, autoriteit 25%
Er is geen verschil voor het soort eenvoudige lussen dat u in een generator-expressie kunt passen. Opbrengst kan echter worden gebruikt om generatoren te maken die veel complexere bewerkingen uitvoeren. Hier is een eenvoudig voorbeeld voor het genereren van de fibonacci-reeks:
>>> 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]
Antwoord 4, autoriteit 13%
Let tijdens gebruik op een onderscheid tussen een generatorobject en een generatorfunctie.
Een generatorobject is eenmalig te gebruiken, in tegenstelling tot een generatorfunctie, die elke keer dat u het opnieuw aanroept opnieuw kan worden gebruikt, omdat het een nieuw generatorobject retourneert.
Generatorexpressies worden in de praktijk meestal “onbewerkt” gebruikt, zonder ze in een functie te stoppen, en ze retourneren een generatorobject.
Bijvoorbeeld:
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()))
die uitvoer:
[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]
Vergelijk met een iets ander gebruik:
range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))
die uitvoer:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
En vergelijk met een generatoruitdrukking:
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))
die ook uitvoert:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
Antwoord 5, autoriteit 11%
Het gebruik van yield
is fijn als de expressie ingewikkelder is dan alleen geneste lussen. U kunt onder andere een bijzondere eerste of bijzondere laatste waarde teruggeven. Overweeg:
def Generator(x):
for i in xrange(x):
yield(i)
yield(None)
Antwoord 6, autoriteit 9%
Ja, er is een verschil.
Voor de generatoruitdrukking (x for var in expr)
, wordt iter(expr)
aangeroepen wanneer de uitdrukking gemaaktis.
Bij gebruik van def
en yield
om een generator te maken, zoals in:
def my_generator():
for var in expr:
yield x
g = my_generator()
iter(expr)
is nog niet aangeroepen. Het wordt alleen aangeroepen bij iteratie op g
(en wordt mogelijk helemaal niet aangeroepen).
Deze iterator als voorbeeld nemen:
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
Deze code:
g1 = (i ** 2 for i in CountDown(3)) # immediately prints "ITER"
print("Go!")
for x in g1:
print(x)
terwijl:
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)
Omdat de meeste iterators niet veel dingen doen in __iter__
, is het gemakkelijk om dit gedrag over het hoofd te zien. Een voorbeeld uit de praktijk is Django’s QuerySet
, die het ophalen van gegevens in __iter__
en data = (f(x) for x in qs)
kan veel tijd kosten, terwijl def g(): for x in qs: yield f(x)
gevolgd door data=g()
onmiddellijk zou terugkeren.
Voor meer info en de formele definitie verwijzen we naar PEP 289 — Generator Expressions.
Antwoord 7, autoriteit 7%
Als je aan iterators denkt, de module itertools
:
… standaardiseert een kernset van snelle, geheugenefficiënte tools die op zichzelf of in combinatie nuttig zijn. Samen vormen ze een “iteratoralgebra” die het mogelijk maakt om gespecialiseerde tools beknopt en efficiënt te construeren in pure Python.
Overweeg voor prestaties itertools.product(*iterables[, repeat])
Cartesiaans product van invoer-iterables.
Equivalent aan geneste for-lussen in een generator-expressie.
product(A, B)
geeft bijvoorbeeld hetzelfde terug als((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)]
>>>