Czy w Pythonie jest jakaś różnica między tworzeniem obiektu generatora za pomocą wyrażenia generatora a użyciem instrukcji yield ?
Korzystanie z wydajności :
def Generator(x, y):
for i in xrange(x):
for j in xrange(y):
yield(i, j)
Korzystanie z 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 tworzą krotki, np. (0,0), (0,1) itd.
Jakieś zalety jednej lub drugiej? Myśli?
Dzięki wszystkim! W tych odpowiedziach jest wiele świetnych informacji i dalszych odniesień!
python
python-3.x
generator
yield
cschol
źródło
źródło
Odpowiedzi:
Są tylko niewielkie różnice w tych dwóch. Możesz użyć
dis
modułu do samodzielnego zbadania tego rodzaju rzeczy.Edycja: moja pierwsza wersja dekompilowała wyrażenie generatora utworzone w zakresie modułu w interaktywnym zachęcie. To nieco różni się od wersji OP, która jest używana wewnątrz funkcji. Zmodyfikowałem to, aby pasowało do rzeczywistego przypadku w pytaniu.
Jak widać poniżej, generator „wydajności” (pierwszy przypadek) ma trzy dodatkowe instrukcje w konfiguracji, ale od pierwszej
FOR_ITER
różnią się one tylko jednym względem: podejście „uzysku” używaLOAD_FAST
zamiast aLOAD_DEREF
wewnątrz pętli.LOAD_DEREF
Jest „a mniejsza” niżLOAD_FAST
, co oznacza, że ten „wydajność” wersji nieco szybciej niż ekspresji generatora dostatecznie dużych wartościachx
(zewnętrzna pętla), ponieważ wartośćy
jest ładowany nieco szybciej w każdym przejściu. W przypadku mniejszych wartościx
byłby nieco wolniejszy z powodu dodatkowego narzutu kodu konfiguracji.Warto również zwrócić uwagę, że wyrażenie generatora jest zwykle używane w kodzie, zamiast opakowywać je w taką funkcję. Pozwoliłoby to usunąć trochę narzutów związanych z konfiguracją i sprawić, że wyrażenie generatora będzie nieco szybsze dla mniejszych wartości pętli, nawet jeśli w przeciwnym razie
LOAD_FAST
zapewniłoby to przewagę wersji „yield”.W żadnym przypadku różnica w wydajności nie byłaby wystarczająca, aby uzasadnić wybór między jednym a drugim. Czytelność liczy się znacznie bardziej, więc używaj tego, co wydaje się 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
źródło
LOAD_DEREF
jest „raczej wolniejsze”, więc jeśli wydajność naprawdę ma znaczenie, to pewne rzeczywiste timingtimeit
byłoby dobre. Analiza teoretyczna idzie tylko na razie.W tym przykładzie nie do końca. Ale
yield
może być używany do bardziej złożonych konstrukcji - na przykład może również akceptować wartości od wywołującego i w rezultacie modyfikować przepływ. Przeczytaj PEP 342, aby uzyskać więcej informacji (jest to interesująca technika, którą warto poznać).W każdym razie najlepszą radą jest użycie tego, co jest bardziej zrozumiałe dla twoich potrzeb .
PS Oto prosty przykład programu autorstwa Dave'a Beazleya :
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!")
źródło
Nie ma różnicy w rodzaju prostych pętli, które można dopasować do wyrażenia generatora. Jednak wydajność można wykorzystać do tworzenia generatorów, które wykonują znacznie bardziej złożone przetwarzanie. Oto prosty przykład generowania sekwencji Fibonacciego:
>>> 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]
źródło
W użyciu należy zwrócić uwagę na różnicę między obiektem generatora a funkcją generatora.
Obiekt generatora jest używany tylko raz, w przeciwieństwie do funkcji generatora, której można użyć ponownie za każdym razem, gdy wywołujesz go ponownie, ponieważ zwraca nowy obiekt generatora.
Wyrażenia generatora są w praktyce zwykle używane jako „surowe”, bez zawijania ich w funkcję, i zwracają obiekt generatora.
Na przykład:
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ścia:
[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 innym zastosowaniem:
które wyjścia:
[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óry również wyprowadza:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] []
źródło
Używanie
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ść. Rozważać:def Generator(x): for i in xrange(x): yield(i) yield(None)
źródło
Myśląc o iteratorach,
itertools
moduł:Aby uzyskać wydajność, rozważ
itertools.product(*iterables[, repeat])
>>> 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)] >>>
źródło
Tak, jest różnica.
Dla wyrażenia generatora
(x for var in expr)
,iter(expr)
jest wywoływana, gdy wyrażenie jest tworzony .Podczas używania
def
iyield
tworzenia generatora, jak w:def my_generator(): for var in expr: yield x g = my_generator()
iter(expr)
nie jest jeszcze wezwany. Będzie wywoływana tylko podczas iteracjig
(i może nie zostać wywołana w ogóle).Biorąc ten iterator jako przykład:
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)
podczas:
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 wykonuje wielu czynności
__iter__
, łatwo jest przeoczyć to zachowanie. Prawdziwym przykładem może być DjangoQuerySet
, który pobiera dane__iter__
idata = (f(x) for x in qs)
może zająć dużo czasu, adef g(): for x in qs: yield f(x)
po nimdata=g()
natychmiast powraca.Więcej informacji i formalną definicję można znaleźć w PEP 289 - Wyrażenia generatora .
źródło
Istnieje różnica, która może być ważna w niektórych kontekstach, a która nie została jeszcze wskazana. Używanie
yield
zapobiega używaniureturn
do czegoś innego niż niejawne podnoszenie StopIteration (i powiązanych rzeczy) .Oznacza to, że ten kod jest źle sformułowany (a przekazanie go tłumaczowi 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)
źródło