V tomto kurzu se naučíte, jak snadno vytvářet iterace pomocí generátorů Pythonu, jak se liší od iterátorů a běžných funkcí a proč byste je měli používat.
Video: Generátory Pythonu
Generátory v Pythonu
Při vytváření iterátoru v Pythonu je spousta práce. Musíme zavést třídu __iter__()
a __next__()
metodu, mít přehled o vnitřních stavů a zvyšovat StopIteration
, když nejsou k dispozici žádné hodnoty, které mají být vráceny.
Je to zdlouhavé a neintuitivní. V takových situacích generátor přijde na pomoc.
Generátory Pythonu jsou jednoduchý způsob vytváření iterátorů. Veškerá práce, kterou jsme zmínili výše, je automaticky zpracována generátory v Pythonu.
Jednoduše řečeno, generátor je funkce, která vrací objekt (iterátor), který můžeme iterovat (jednu hodnotu po druhé).
Vytvořte generátory v Pythonu
Vytvoření generátoru v Pythonu je poměrně jednoduché. Je to stejně snadné jako definovat normální funkci, ale s yield
příkazem místo s return
příkazem.
Pokud funkce obsahuje alespoň jeden yield
příkaz (může obsahovat další yield
nebo return
příkazy), stane se funkcí generátoru. Oba yield
a return
vrátí určitou hodnotu z funkce.
Rozdíl je v tom, že zatímco return
příkaz úplně ukončí funkci, yield
příkaz pozastaví funkci a uloží všechny její stavy a později odtud pokračuje při následných voláních.
Rozdíly mezi funkcí generátoru a normální funkcí
Zde se liší funkce generátoru od normální funkce.
- Funkce generátoru obsahuje jeden nebo více
yield
příkazů. - Při volání vrátí objekt (iterátor), ale nespustí spuštění okamžitě.
- Metody jako
__iter__()
a__next__()
jsou implementovány automaticky. Můžeme tedy iterovat pomocí položek pomocínext()
. - Jakmile se funkce získá, funkce se pozastaví a ovládací prvek se přenese volajícímu.
- Místní proměnné a jejich stavy si pamatují mezi po sobě jdoucími voláními.
- Nakonec, když funkce skončí,
StopIteration
je automaticky vyvolána při dalších voláních.
Zde je příklad pro ilustraci všech výše uvedených bodů. Máme funkci generátoru pojmenovanou my_gen()
několika yield
příkazy.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n
Interaktivní běh v tlumočníkovi je uveden níže. Spuštěním v prostředí Pythonu zobrazíte výstup.
>>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration
Jedna zajímavá věc, kterou je třeba si všimnout ve výše uvedeném příkladu, je, že hodnota proměnné n je mezi každým voláním zapamatována.
Na rozdíl od normálních funkcí se místní proměnné při výnosu funkce nezničí. Objekt generátoru lze dále iterovat pouze jednou.
Chcete-li restartovat proces, musíme vytvořit další objekt generátoru pomocí něčeho podobného a = my_gen()
.
Jedna poslední věc, kterou je třeba poznamenat, je, že můžeme použít generátory s pro smyčky přímo.
Je to proto, že for
smyčka přebírá iterátor a iteruje ho pomocí next()
funkce. Automaticky končí, když StopIteration
je zvednutý. Zkontrolujte zde, abyste věděli, jak je smyčka for ve skutečnosti implementována v Pythonu.
# A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)
Když spustíte program, výstup bude:
Toto se vytiskne jako první 1 Toto se vytiskne jako druhé 2 Toto se vytiskne jako poslední 3
Generátory Pythonu se smyčkou
Výše uvedený příklad je méně užitečný a studovali jsme ho, abychom získali představu o tom, co se děje v pozadí.
Normálně jsou funkce generátoru implementovány se smyčkou, která má vhodný koncový stav.
Vezměme si příklad generátoru, který obrátí řetězec.
def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)
Výstup
olleh
V tomto příkladu jsme použili range()
funkci k získání indexu v opačném pořadí pomocí smyčky for.
Poznámka : Tato funkce generátoru funguje nejen s řetězci, ale také s jinými druhy iterable, jako je list, n-tice atd.
Výraz generátoru Pythonu
Jednoduché generátory lze snadno vytvářet za běhu pomocí výrazů generátoru. Vytváření generátorů je snadné.
Podobně jako funkce lambda, které vytvářejí anonymní funkce, vytvářejí výrazy generátoru anonymní funkce generátoru.
Syntaxe výrazu generátoru je podobná syntaxi s porozuměním seznamu v Pythonu. Hranaté závorky jsou ale nahrazeny kulatými závorkami.
Hlavní rozdíl mezi porozuměním seznamu a výrazem generátoru spočívá v tom, že porozumění seznamu vytváří celý seznam, zatímco výraz generátoru vytváří jednu položku najednou.
They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.
# Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)
Output
(1, 9, 36, 100)
We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.
Here is how we can start getting items from the generator:
# Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)
When we run the above program, we get the following output:
1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration
Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
Use of Python Generators
There are several reasons that make generators a powerful implementation.
1. Easy to Implement
Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.
class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result
The above program was lengthy and confusing. Now, let's do the same using a generator function.
def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1
Since generators keep track of details automatically, the implementation was concise and much cleaner.
2. Memory Efficient
A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.
Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.
3. Represent Infinite Stream
Generátory jsou vynikající média, která představují nekonečný proud dat. Nekonečné streamy nelze uložit do paměti a protože generátory produkují pouze jednu položku najednou, mohou představovat nekonečný proud dat.
Následující funkce generátoru může generovat všechna sudá čísla (alespoň teoreticky).
def all_even(): n = 0 while True: yield n n += 2
4. Potrubí generátory
K pipeline řady operací lze použít více generátorů. To je nejlépe ilustrováno na příkladu.
Předpokládejme, že máme generátor, který produkuje čísla v řadě Fibonacci. A máme další generátor pro umocnění čísel.
Chceme-li zjistit součet čtverců čísel v řadě Fibonacci, můžeme to udělat následujícím způsobem tím, že zkopírujeme výstup funkcí generátoru dohromady.
def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))
Výstup
4895
Toto pipeline je efektivní a snadno čitelné (a ano, mnohem chladnější!).