Python: wyrażenie generatora a wydajność
W Pythonie, czy istnieje jakakolwiek różnica między tworzeniem obiektu generatora poprzez wyrażenie generatora a używaniem instrukcji yield?
Użycie wydajność :
def Generator(x, y):
for i in xrange(x):
for j in xrange(y):
yield(i, j)
Użycie wyrażenia generatora :
def Generator(x, y):
return ((i, j) for i in xrange(x) for j in xrange(y))
Obie funkcje zwracają obiekty generatora, które wytwarzają krotki, np. (0,0), (0,1) itd.
Jakieś zalety jednego lub drugiego? Myśli?
Dzięki wszystkim! Jest wiele świetnych informacji i kolejne odniesienia w tych odpowiedziach!
8 answers
Istnieją tylko niewielkie różnice w tych dwóch. Możesz użyć modułu dis
do samodzielnego zbadania tego typu rzeczy.
Edit: moja pierwsza wersja dekompilowała wyrażenie generatora utworzone w module-scope w interaktywnym monicie. To nieco różni się od wersji OP z używanym wewnątrz funkcji. Zmodyfikowałem to tak, aby pasowało do konkretnej sprawy w pytaniu.
Jak widać poniżej, generator "plon" (pierwszy przypadek) ma trzy dodatkowe instrukcje w konfiguracji, ale od pierwszej FOR_ITER
różnią się tylko pod jednym względem: podejście "yield" używa LOAD_FAST
zamiast LOAD_DEREF
wewnątrz pętli. LOAD_DEREF
jest "raczej wolniejszym" niż LOAD_FAST
, więc sprawia, że wersja "yield" jest nieco szybsza niż wyrażenie generatora dla wystarczająco dużych wartości x
(pętli zewnętrznej), ponieważ wartość y
jest ładowana nieco szybciej na każdym przejściu. Dla mniejszych wartości x
byłoby to nieco wolniejsze ze względu na dodatkowe obciążenie kod konfiguracji.
Warto również zwrócić uwagę, że wyrażenie generatora zwykle będzie używane w kodzie w linii prostej, zamiast owijać je taką funkcją. To usunęłoby trochę z góry konfiguracji i utrzymałoby wyrażenie generatora nieco szybciej dla mniejszych wartości pętli, nawet jeśli LOAD_FAST
dało wersję "yield" przewagę w przeciwnym razie.
W żadnym przypadku różnica w wydajności nie byłaby wystarczająca, aby uzasadnić podjęcie decyzji między jednym a drugim. Czytelność liczy się znacznie więcej, więc używaj tego, co jest najbardziej czytelne w danej sytuacji.
>>> 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 18:22:10
W tym przykładzie nie do końca. Ale yield
może być używany dla bardziej złożonych konstrukcji - na przykład może również przyjmować wartości z wywołującego i modyfikować przepływ w wyniku. Przeczytaj PEP 342 Po Więcej Szczegółów (to ciekawa technika, którą warto znać).
W każdym razie, najlepszą radą jest użyj tego, co jest jaśniejsze dla Twoich potrzeb.
P. S. Oto prosty przykład z 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!")
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:18:36
Nie ma różnicy w rodzaju prostych pętli, które można zmieścić w wyrażeniu generatora. Jednak wydajność może być wykorzystana do tworzenia generatorów, które wykonują znacznie bardziej złożone przetwarzanie. Oto prosty przykład generowania ciągu Fibonacciego:
>>> 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]
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:30:39
Użycie yield
jest przyjemne, jeśli wyrażenie jest bardziej skomplikowane niż tylko zagnieżdżone pętle. Między innymi możesz zwrócić specjalną pierwszą lub specjalną ostatnią wartość. Consider:
def Generator(x):
for i in xrange(x):
yield(i)
yield(None)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:13:13
W użyciu należy zauważyć rozróżnienie między obiektem generatora a funkcją generatora.
Obiekt generatora jest use-once-only, W przeciwieństwie do funkcji generatora, która może być ponownie użyta za każdym razem, gdy zostanie wywołana ponownie, ponieważ zwraca nowy obiekt generatora.
Wyrażenia generatora są w praktyce zwykle używane "raw", bez zawijania ich w funkcję i zwracają obiekt generatora.
Np.:
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()))
Które wyjście:
[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]
Porównaj z nieco inne zastosowanie:
range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))
Które wyjście:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
I porównaj z wyrażeniem generatora:
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))
, które również wyprowadza:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-12 06:45:23
Myśląc o iteratorach, moduł itertools
:
... standaryzuje podstawowy zestaw szybkich, wydajnych pamięci narzędzi, które są przydatne same lub w połączeniu. Razem tworzą "algebrę iteratora", dzięki czemu możliwe jest zwięzłe i wydajne konstruowanie specjalistycznych narzędzi w czystym Pythonie.
Dla wydajności, rozważ itertools.product(*iterables[, repeat])
Iloczyn kartezjański iterabli wejściowych.
Odpowiednik zagnieżdżonych pętli for-loops w generatorze ekspresja. Na przykład,
product(A, B)
zwraca to samo co((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)]
>>>
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:21:44
Tak, jest różnica.
Dla wyrażenia generatora(x for var in expr)
, iter(expr)
jest wywoływane, gdy wyrażenie jest utworzone .
Przy użyciu def
i yield
do utworzenia generatora, jak w:
def my_generator():
for var in expr:
yield x
g = my_generator()
iter(expr)
nie został jeszcze wezwany. Zostanie wywołana tylko podczas iteracji na g
(i może nie być wywołana w ogóle).
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
Ten kod:
g1 = (i ** 2 for i in CountDown(3)) # immediately prints "ITER"
print("Go!")
for x in g1:
print(x)
While:
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)
Ponieważ większość iteratorów nie wiele rzeczy w __iter__
, łatwo jest przegapić takie zachowanie. Prawdziwym przykładem może być QuerySet
Django, które pobiera dane w __iter__
i data = (f(x) for x in qs)
może zająć dużo czasu, podczas gdy def g(): for x in qs: yield f(x)
a następnie data=g()
powróci natychmiast.
Więcej informacji i formalna definicja znajdują się w PEP 289 -- wyrażenia generatora.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-17 23:10:21
Istnieje różnica, która może być ważna w niektórych kontekstach, które jeszcze nie zostały wskazane. Używanie yield
zapobiega używaniu return
do czegoś innego niż w domyśle podnoszenie Stopiteracji (i rzeczy związanych z koroutinami).
Oznacza to, że ten kod jest źle uformowany (a podanie go interpreterowi da ci 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)
Z drugiej strony, ten kod działa jak urok:
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)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:19