Python Language
generatoren
Zoeken…
Invoering
Generators zijn luie iterators gemaakt door generatorfuncties (met yield
) of generatoruitdrukkingen (met (an_expression for x in an_iterator)
).
Syntaxis
- opbrengst
<expr>
- opbrengst van
<expr>
-
<var>
= opbrengst<expr>
- volgende (
<iter>
)
herhaling
Een generatorobject ondersteunt het iteratorprotocol . Dat wil zeggen, het biedt een next()
-methode ( __next__()
in Python 3.x), die wordt gebruikt om de uitvoering ervan te __iter__
en de __iter__
methode retourneert zichzelf. Dit betekent dat een generator kan worden gebruikt in elke taalconstructie die generieke iterabele objecten ondersteunt.
# naive partial implementation of the Python 2.x xrange()
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# looping
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
De functie next ()
De next()
ingebouwde is een handige wrapper die kan worden gebruikt om een waarde te ontvangen van elke iterator (inclusief een generator-iterator) en om een standaardwaarde te bieden in het geval dat de iterator is uitgeput.
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
De syntaxis is de next(iterator[, default])
. Als iterator eindigt en een standaardwaarde is doorgegeven, wordt deze geretourneerd. Als er geen standaard is opgegeven, wordt StopIteration
verhoogd.
Objecten naar een generator sturen
Naast het ontvangen van waarden van een generator, is het mogelijk om een object naar een generator te verzenden met behulp van de methode send()
.
def accumulator():
total = 0
value = None
while True:
# receive sent value
value = yield total
if value is None: break
# aggregate values
total += value
generator = accumulator()
# advance until the first "yield"
next(generator) # 0
# from this point on, the generator aggregates values
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# Calling next(generator) is equivalent to calling generator.send(None)
next(generator) # StopIteration
Wat hier gebeurt is het volgende:
- Wanneer u
next(generator)
voor het eerstyield
, gaat het programma naar de eersteyield
en retourneert de waarde vantotal
op dat punt, dat 0 is. De uitvoering van de generator wordt op dit punt onderbroken. - Wanneer u vervolgens
generator.send(x)
aanroept, neemt de interpreter het argumentx
en maakt dit de retourwaarde van de laatsteyield
, die wordt toegewezen aanvalue
. De generator gaat dan gewoon door tot hij de volgende waarde oplevert. - Wanneer je eindelijk
next(generator)
, behandelt het programma dit alsof jeNone
naar de generator stuurt. Er is niets speciaals aanNone
, maar dit voorbeeld gebruiktNone
als een speciale waarde om de generator te vragen te stoppen.
Generator-uitdrukkingen
Het is mogelijk om generator-iterators te maken met behulp van een begripachtige syntaxis.
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
Als een functie niet noodzakelijkerwijs aan een lijst moet worden doorgegeven, kunt u op tekens besparen (en de leesbaarheid verbeteren) door een generatoruitdrukking in een functieaanroep te plaatsen. De haakjes van de functieaanroep maken impliciet van uw uitdrukking een generatoruitdrukking.
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
Bovendien bespaart u geheugen omdat in plaats van de hele lijst te laden waarover u itereert ( [0, 1, 2, 3]
in het bovenstaande voorbeeld), de generator Python toestaat om waar nodig waarden te gebruiken.
Invoering
Generator-uitdrukkingen zijn vergelijkbaar met lijst-, woordenboek- en setbegrippen, maar staan tussen haakjes. De haakjes hoeven niet aanwezig te zijn wanneer ze worden gebruikt als het enige argument voor een functieaanroep.
expression = (x**2 for x in range(10))
Dit voorbeeld genereert de 10 eerste perfecte vierkanten, inclusief 0 (waarin x = 0).
Generatorfuncties zijn vergelijkbaar met reguliere functies, behalve dat ze een of meer yield
in hun lichaam hebben. Dergelijke functies kunnen geen waarden return
(lege return
zijn echter toegestaan als u de generator vroegtijdig wilt stoppen).
def function():
for x in range(10):
yield x**2
Deze generatorfunctie is gelijk aan de vorige generatoruitdrukking, deze voert hetzelfde uit.
Opmerking : alle generatoruitdrukkingen hebben hun eigen equivalente functies, maar niet andersom.
Een generatoruitdrukking kan zonder haakjes worden gebruikt als beide haakjes anders zouden worden herhaald:
sum(i for i in range(10) if i % 2 == 0) #Output: 20
any(x = 0 for x in foo) #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) #Output: <class 'generator'>
In plaats van:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
Maar niet:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
Het aanroepen van een generatorfunctie produceert een generatorobject , dat later kan worden herhaald. In tegenstelling tot andere typen iterators, kunnen generatorobjecten slechts eenmaal worden verplaatst.
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
Merk op dat de body van een generator niet onmiddellijk wordt uitgevoerd: wanneer u function()
in het bovenstaande voorbeeld aanroept, retourneert deze onmiddellijk een generatorobject, zonder zelfs de eerste afdrukopdracht uit te voeren. Hierdoor kunnen generatoren minder geheugen verbruiken dan functies die een lijst retourneren, en kunnen generators worden gemaakt die oneindig lange reeksen produceren.
Om deze reden worden generatoren vaak gebruikt in de gegevenswetenschap en in andere contexten met grote hoeveelheden gegevens. Een ander voordeel is dat andere code onmiddellijk de waarden van een generator kan gebruiken, zonder te wachten tot de volledige reeks is geproduceerd.
Als u de door een generator geproduceerde waarden echter meer dan eens moet gebruiken en als het genereren ervan meer kost dan opslaan, is het wellicht beter om de weergegeven waarden op te slaan als een list
dan om de reeks opnieuw te genereren. Zie 'Een generator resetten' hieronder voor meer informatie.
Gewoonlijk wordt een generatorobject gebruikt in een lus of in een functie waarvoor een iterabele vereist is:
for x in g1:
print("Received", x)
# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Aangezien generatorobjecten iterators zijn, kunt u deze handmatig herhalen met de functie next()
. Als u dit doet, worden de verkregen waarden één voor één geretourneerd bij elke volgende aanroep.
Onder de motorkap voert Python elke keer dat u next()
op een generator aanroept, uit in de body van de generatorfunctie totdat het de volgende yield
raakt. Op dit punt retourneert het het argument van de yield
en onthoudt het punt waar dat gebeurde. Als u next()
opnieuw aanroept, wordt de uitvoering vanaf dat punt hervat en gaat u door tot de volgende yield
.
Als Python het einde van de generatorfunctie bereikt zonder dat er meer yield
s StopIteration
, wordt een StopIteration
uitzondering opgeworpen (dit is normaal, alle iterators gedragen zich op dezelfde manier).
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
Merk op dat in Python 2 generatorobjecten .next()
methoden hadden die konden worden gebruikt om de verkregen waarden handmatig te doorlopen. In Python 3 werd deze methode vervangen door de .__next__()
-standaard voor alle iterators.
Een generator resetten
Vergeet niet dat u slechts eenmaal door de door een generator gegenereerde objecten kunt doorlopen. Als u de objecten in een script al hebt doorgenomen, levert elke verdere poging om None
.
Als u de door een generator gegenereerde objecten meer dan één keer moet gebruiken, kunt u de generatorfunctie opnieuw definiëren en een tweede keer gebruiken, of u kunt de uitvoer van de generatorfunctie bij eerste gebruik opslaan in een lijst. Het opnieuw definiëren van de generatorfunctie is een goede optie als u te maken hebt met grote hoeveelheden gegevens en het opslaan van een lijst met alle gegevensitems veel schijfruimte in beslag zou nemen. Omgekeerd, als het kostbaar is om de items in eerste instantie te genereren, kunt u de gegenereerde items liever opslaan in een lijst zodat u ze opnieuw kunt gebruiken.
Een generator gebruiken om Fibonacci-nummers te vinden
Een praktisch gebruik van een generator is het doorlopen van waarden van een oneindige reeks. Hier is een voorbeeld van het vinden van de eerste tien termen van de Fibonacci-reeks .
def fib(a=0, b=1):
"""Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Oneindige reeksen
Generators kunnen worden gebruikt om oneindige sequenties weer te geven:
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
Een oneindige reeks getallen zoals hierboven kan ook worden gegenereerd met behulp van itertools.count
. De bovenstaande code kan worden geschreven zoals hieronder
natural_numbers = itertools.count(1)
U kunt generatorbegrippen op oneindige generators gebruiken om nieuwe generators te produceren:
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
Houd er rekening mee dat een oneindige generator geen einde heeft, dus het doorgeven aan een functie die zal proberen de generator volledig te verbruiken, heeft ernstige gevolgen :
list(multiples_of_two) # will never terminate, or raise an OS-specific error
Gebruik in plaats daarvan list / set-begrippen met range
(of xrange
voor python <3.0):
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
of gebruik itertools.islice()
om de iterator in een subset op te delen:
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
Merk op dat de originele generator ook wordt bijgewerkt, net als alle andere generators die uit dezelfde "root" komen:
next(natural_numbers) # yields 16
next(multiples_of_two) # yields 34
next(multiples_of_four) # yields 24
Een oneindige reeks kan ook worden herhaald met een for
loop . Zorg ervoor dat u een voorwaardelijke break
instructie opneemt, zodat de lus uiteindelijk wordt beëindigd:
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # stop after taking the first 10 multiplies of two
Klassiek voorbeeld - Fibonacci-nummers
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
Opbrengst van alle waarden van een andere iterabel
Gebruik yield from
als u alle waarden van een andere iterabele wilt opleveren:
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
Dit werkt ook met generatoren.
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
coroutines
Generatoren kunnen worden gebruikt om coroutines te implementeren:
# create and advance generator to the first yield
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# example coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# example use
s = adder()
s.send(1) # 1
s.send(2) # 3
Coroutines worden vaak gebruikt om statusmachines te implementeren, omdat ze vooral nuttig zijn voor het maken van procedures met één methode waarvoor een status correct moet werken. Ze werken in een bestaande status en retourneren de waarde die is verkregen bij voltooiing van de bewerking.
Opbrengst met recursie: recursief een lijst van alle bestanden in een map
Importeer eerst de bibliotheken die met bestanden werken:
from os import listdir
from os.path import isfile, join, exists
Een helpfunctie om alleen bestanden uit een map te lezen:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
Nog een hulpfunctie om alleen de submappen te krijgen:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
Gebruik nu deze functies om recursief alle bestanden in een map en alle bijbehorende submappen te krijgen (met behulp van generatoren):
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # here the recursive call
yield file
Deze functie kan worden vereenvoudigd met behulp van yield from
:
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
Parallel schakelen over generatoren
Gebruik de ingebouwde zip
om meerdere generatoren parallel te doorlopen:
for x, y in zip(a,b):
print(x,y)
Resulteert in:
1 x
2 y
3 z
In python 2 moet u in plaats daarvan itertools.izip
gebruiken. Hier kunnen we ook zien dat alle zip
functies tuples opleveren.
Merk op dat zip stopt met itereren zodra een van de iterables geen items meer heeft. Gebruik itertools.zip_longest()
als je wilt herhalen zo lang als de langste iterabel is.
Refactoring lijstbouwcode
Stel dat u een complexe code hebt die een lijst maakt en retourneert door te beginnen met een lege lijst en deze herhaaldelijk toe te voegen:
def create():
result = []
# logic here...
result.append(value) # possibly in several places
# more logic...
return result # possibly in several places
values = create()
Als het niet praktisch is om de innerlijke logica te vervangen door een lijstbegrip, kunt u de hele functie omzetten in een generator en vervolgens de resultaten verzamelen:
def create_gen():
# logic...
yield value
# more logic
return # not needed if at the end of the function, of course
values = list(create_gen())
Als de logica recursief is, gebruikt u yield from
om alle waarden van de recursieve aanroep op te nemen in een "afgeplat" resultaat:
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
Zoeken
De next
functie is zelfs nuttig zonder itereren. Een generatoruitdrukking doorgeven aan next
is een snelle manier om te zoeken naar het eerste exemplaar van een element dat overeenkomt met een bepaald predicaat. Procedurele code zoals
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
kan worden vervangen door:
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.
Voor dit doel kan het wenselijk zijn om een alias te maken, zoals first = next
, of een wrapper-functie om de uitzondering te converteren:
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError