In deze tutorial leert u hoe u eenvoudig iteraties kunt maken met Python-generatoren, hoe deze verschilt van iterators en normale functies, en waarom u deze zou moeten gebruiken.
Video: Python-generatoren
Generatoren in Python
Er is veel werk aan het bouwen van een iterator in Python. We moeten een klasse implementeren met __iter__()
en __next__()
methode, interne toestanden bijhouden en verhogen StopIteration
als er geen waarden zijn die kunnen worden geretourneerd.
Dit is zowel langdurig als contra-intuïtief. Generator komt in dergelijke situaties te hulp.
Python-generatoren zijn een eenvoudige manier om iterators te maken. Al het werk dat we hierboven noemden, wordt automatisch afgehandeld door generatoren in Python.
Simpel gezegd is een generator een functie die een object (iterator) retourneert waarover we kunnen herhalen (één waarde tegelijk).
Maak generatoren in Python
Het is vrij eenvoudig om een generator in Python te maken. Het is net zo eenvoudig als het definiëren van een normale functie, maar met een yield
statement in plaats van een return
statement.
Als een functie ten minste één yield
instructie bevat (het kan andere yield
of return
instructies bevatten ), wordt het een generatorfunctie. Beide yield
en return
zullen een waarde van een functie retourneren.
Het verschil is dat terwijl een return
instructie een functie volledig beëindigt, de yield
instructie de functie pauzeert en al zijn statussen opslaat en later verder gaat met opeenvolgende aanroepen.
Verschillen tussen generatorfunctie en normale functie
Hier is hoe een generatorfunctie verschilt van een normale functie.
- Generatorfunctie bevat een of meer
yield
instructies. - Wanneer het wordt aangeroepen, wordt een object (iterator) geretourneerd maar wordt de uitvoering niet onmiddellijk gestart.
- Methoden zoals
__iter__()
en__next__()
worden automatisch geïmplementeerd. Dus we kunnen de items doorlopen metnext()
. - Zodra de functie het oplevert, wordt de functie gepauzeerd en wordt de controle overgedragen aan de beller.
- Lokale variabelen en hun status worden tussen opeenvolgende oproepen onthouden.
- Ten slotte, wanneer de functie wordt beëindigd,
StopIteration
wordt automatisch verhoogd bij verdere oproepen.
Hier is een voorbeeld om alle bovenstaande punten te illustreren. We hebben een generatorfunctie my_gen()
met verschillende yield
instructies.
# 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
Hieronder wordt een interactieve run in de tolk gegeven. Voer deze uit in de Python-shell om de uitvoer te zien.
>>> # 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
Een interessant ding om op te merken in het bovenstaande voorbeeld is dat de waarde van variabele n tussen elke oproep wordt onthouden.
In tegenstelling tot normale functies worden de lokale variabelen niet vernietigd als de functie oplevert. Bovendien kan het generatorobject slechts één keer worden herhaald.
Om het proces opnieuw te starten, moeten we een ander generatorobject maken met zoiets als a = my_gen()
.
Een laatste ding om op te merken is dat we generatoren rechtstreeks met for-loops kunnen gebruiken.
Dit komt doordat een for
lus een iterator nodig heeft en deze herhaalt met behulp van de next()
functie. Het eindigt automatisch wanneer StopIteration
wordt verhoogd. Kijk hier om te weten hoe een for-lus daadwerkelijk is geïmplementeerd in Python.
# 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)
Wanneer u het programma uitvoert, is de uitvoer:
Dit wordt als eerste geprint 1 Dit wordt als tweede geprint 2 Dit wordt als laatste geprint 3
Python-generatoren met een lus
Het bovenstaande voorbeeld is van minder nut en we hebben het bestudeerd om een idee te krijgen van wat er op de achtergrond gebeurde.
Normaal gesproken worden generatorfuncties geïmplementeerd met een lus met een geschikte afsluitconditie.
Laten we een voorbeeld nemen van een generator die een string omkeert.
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)
Uitvoer
olleh
In dit voorbeeld hebben we de range()
functie gebruikt om de index in omgekeerde volgorde op te halen met de for-lus.
Opmerking : deze generatorfunctie werkt niet alleen met strings, maar ook met andere soorten iterables zoals list, tuple, etc.
Python Generator-expressie
Eenvoudige generatoren kunnen eenvoudig tijdens het gebruik worden gemaakt met behulp van generatoruitdrukkingen. Het maakt het bouwen van generatoren eenvoudig.
Net als de lambda-functies die anonieme functies creëren, creëren generatoruitdrukkingen anonieme generatorfuncties.
De syntaxis voor generatoruitdrukking is vergelijkbaar met die van lijstbegrip in Python. Maar de vierkante haken worden vervangen door ronde haakjes.
Het belangrijkste verschil tussen een lijstbegrip en een generatoruitdrukking is dat een lijstbegrip de hele lijst produceert, terwijl de generatoruitdrukking één item tegelijk produceert.
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
Generatoren zijn uitstekende media om een oneindige stroom gegevens weer te geven. Oneindige stromen kunnen niet in het geheugen worden opgeslagen, en aangezien generatoren slechts één item per keer produceren, kunnen ze een oneindige stroom gegevens vertegenwoordigen.
De volgende generatorfunctie kan alle even getallen genereren (tenminste in theorie).
def all_even(): n = 0 while True: yield n n += 2
4. Generatoren voor pijpleidingen
Meerdere generatoren kunnen worden gebruikt om een reeks bewerkingen te pijpleidingen. Dit wordt het best geïllustreerd aan de hand van een voorbeeld.
Stel dat we een generator hebben die de getallen in de Fibonacci-reeks produceert. En we hebben nog een generator voor het kwadrateren van getallen.
Als we de som van de kwadraten van getallen in de Fibonacci-reeks willen achterhalen, kunnen we dit op de volgende manier doen door de uitvoer van generatorfuncties samen te pipelineeren.
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))))
Uitvoer
4895
Deze pipelining is efficiënt en gemakkelijk af te lezen (en ja, veel cooler!).