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 eerst yield , gaat het programma naar de eerste yield en retourneert de waarde van total 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 argument x en maakt dit de retourwaarde van de laatste yield , die wordt toegewezen aan value . De generator gaat dan gewoon door tot hij de volgende waarde oplevert.
  • Wanneer je eindelijk next(generator) , behandelt het programma dit alsof je None naar de generator stuurt. Er is niets speciaals aan None , maar dit voorbeeld gebruikt None 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

Python 3.x 3.3

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


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow