Suche…


Einführung

Generatoren sind faule Iteratoren, die von Generatorfunktionen (mit yield ) oder Generatorausdrücken (mit (an_expression for x in an_iterator) ) erstellt werden.

Syntax

  • Ausbeute <expr>
  • Ertrag von <expr>
  • <var> = Ertrag <expr>
  • next ( <iter> )

Iteration

Ein Generatorobjekt unterstützt das Iteratorprotokoll . Das heißt, es stellt eine next() Methode ( __next__() in Python 3.x) __next__() , die zum schrittweisen Durchlaufen der Ausführung verwendet wird, und ihre __iter__ Methode gibt sich selbst zurück. Dies bedeutet, dass ein Generator in einem beliebigen Sprachkonstrukt verwendet werden kann, das generische iterierbare Objekte unterstützt.

# 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]

Die nächste () Funktion

Das next() eingebaute ist ein praktischer Wrapper, der verwendet werden kann, um einen Wert von einem beliebigen Iterator (einschließlich eines Generator-Iterators) zu empfangen und einen Standardwert anzugeben, falls der Iterator erschöpft ist.

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
# ...

Die Syntax ist next(iterator[, default]) . Wenn der Iterator endet und ein Standardwert übergeben wurde, wird er zurückgegeben. Wenn kein Standardwert angegeben wurde, wird StopIteration .

Objekte an einen Generator senden

Zusätzlich zum Empfangen von Werten von einem Generator ist es möglich , ein Objekt mit der send() -Methode an einen Generator zu 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

Was hier passiert, ist folgendes:

  • Wenn Sie zum ersten Mal next(generator) aufrufen, next(generator) das Programm zur ersten yield Anweisung und gibt den Wert von total an diesem Punkt zurück, dh 0. Die Ausführung des Generators wird an diesem Punkt ausgesetzt.
  • Wenn Sie dann generator.send(x) aufrufen, übernimmt der Interpreter das Argument x und macht es zum Rückgabewert der letzten yield Anweisung, die dem value zugewiesen value . Der Generator fährt dann wie gewohnt fort, bis er den nächsten Wert ergibt.
  • Wenn Sie schließlich next(generator) aufrufen, behandelt das Programm dies so, als würden Sie None an den Generator senden. None ist nichts Besonderes. In diesem Beispiel wird jedoch None als spezieller Wert verwendet, um den Generator zum Anhalten aufzufordern.

Generatorausdrücke

Es ist möglich, Generator-Iteratoren mit einer verständnisartigen Syntax zu erstellen.

generator = (i * 2 for i in range(3))

next(generator)  # 0
next(generator)  # 2
next(generator)  # 4
next(generator)  # raises StopIteration

Wenn einer Funktion nicht unbedingt eine Liste übergeben werden muss, können Sie Zeichen speichern (und die Lesbarkeit verbessern), indem Sie einen Generatorausdruck in einen Funktionsaufruf einfügen. Die Klammer aus dem Funktionsaufruf macht Ihren Ausdruck implizit zu einem Generatorausdruck.

sum(i ** 2 for i in range(4))  # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14

Darüber hinaus sparen Sie Speicherplatz, da Sie nicht die gesamte Liste laden, über die Sie iterieren ( [0, 1, 2, 3] im obigen Beispiel), der Generator die Verwendung von Werten durch Python zulässt.

Einführung

Generatorausdrücke ähneln Listen-, Wörterbuch- und Mengenverstehen, sind jedoch in Klammern eingeschlossen. Die Klammern müssen nicht vorhanden sein, wenn sie als einziges Argument für einen Funktionsaufruf verwendet werden.

expression = (x**2 for x in range(10))

In diesem Beispiel werden die 10 ersten perfekten Quadrate generiert, einschließlich 0 (wobei x = 0 ist).

Generatorfunktionen sind regulären Funktionen ähnlich, außer dass sie eine oder mehrere yield in ihrem Körper haben. Solche Funktionen können nicht return alle Werte (jedoch leer return s erlaubt sind , wenn Sie den Generator früh stoppen wollen).

def function():
    for x in range(10):
        yield x**2

Diese Generatorfunktion entspricht dem vorherigen Generatorausdruck und gibt denselben aus.

Hinweis : Alle Generatorausdrücke haben ihre eigenen äquivalenten Funktionen, nicht jedoch umgekehrt.


Ein Generatorausdruck kann ohne Klammern verwendet werden, wenn sich sonst beide Klammern wiederholen würden:

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'>

Anstatt:

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))

Aber nicht:

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)

Beim Aufruf einer Generatorfunktion wird ein Generatorobjekt erzeugt, das später wiederholt werden kann. Im Gegensatz zu anderen Iteratortypen können Generatorobjekte nur einmal durchlaufen werden.

g1 = function()
print(g1)  # Out: <generator object function at 0x1012e1888>

Beachten Sie, dass der Rumpf eines Generators nicht sofort ausgeführt wird: Wenn Sie function() im obigen Beispiel aufrufen, gibt er sofort ein Generatorobjekt zurück, ohne die erste Druckanweisung auszuführen. Dadurch können Generatoren weniger Speicher verbrauchen als Funktionen, die eine Liste zurückgeben. Außerdem können Generatoren erstellt werden, die unendlich lange Sequenzen erzeugen.

Aus diesem Grund werden Generatoren häufig in der Datenwissenschaft und in anderen Kontexten mit großen Datenmengen verwendet. Ein weiterer Vorteil ist, dass anderer Code die von einem Generator ausgegebenen Werte sofort verwenden kann, ohne auf die Erstellung der vollständigen Sequenz zu warten.

Wenn Sie jedoch die von einem Generator erzeugten Werte mehr als einmal verwenden müssen und die Erzeugung dieser Werte mehr kostet als das Speichern, ist es möglicherweise besser, die ermittelten Werte als list zu speichern, als die Sequenz neu zu generieren. Weitere Informationen finden Sie unten unter 'Generator zurücksetzen'.

In der Regel wird ein Generatorobjekt in einer Schleife oder in einer Funktion verwendet, die eine Iteration erfordert:

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]

Da Generatorobjekte Iteratoren sind, kann man sie mit der next() Funktion manuell durchlaufen. Dadurch werden die ermittelten Werte bei jedem nachfolgenden Aufruf einzeln zurückgegeben.

Unter der Haube führt Python bei jedem Aufruf von next() in einem Generator Anweisungen im Rumpf der Generatorfunktion aus, bis die nächste yield . An diesem Punkt wird das Argument des yield und der Punkt, an dem dies geschehen ist, gespeichert. Wenn Sie next() erneut aufrufen, wird die Ausführung ab diesem Punkt fortgesetzt und bis zur nächsten yield Anweisung fortgesetzt.

Wenn Python das Ende der Generatorfunktion erreicht, ohne weitere yield , wird eine StopIteration Ausnahme StopIteration (dies ist normal, alle Iteratoren verhalten sich auf dieselbe Weise).

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

Beachten Sie, dass Generatorobjekte in Python 2 über .next() -Methoden verfügten, mit denen die ermittelten Werte manuell durchlaufen werden konnten. In Python 3 wurde diese Methode für alle Iteratoren durch den Standard .__next__() .

Generator zurücksetzen

Denken Sie daran , dass Sie nur durch die Objekte , die von einem Generator einmal laufen können. Wenn Sie die Objekte in einem Skript bereits durchlaufen haben, führt jeder weitere Versuch zu None .

Wenn Sie die von einem Generator generierten Objekte mehr als einmal verwenden müssen, können Sie entweder die Generatorfunktion erneut definieren und ein zweites Mal verwenden, oder Sie können die Ausgabe der Generatorfunktion bei der ersten Verwendung in einer Liste speichern. Eine erneute Definition der Generatorfunktion ist eine gute Option, wenn Sie mit großen Datenmengen arbeiten und das Speichern einer Liste aller Datenelemente viel Speicherplatz beanspruchen würde. Umgekehrt, wenn die anfängliche Generierung der Elemente teuer ist, können Sie es vorziehen, die generierten Elemente in einer Liste zu speichern, damit Sie sie wiederverwenden können.

Verwenden eines Generators, um Fibonacci-Nummern zu finden

Ein praktischer Anwendungsfall eines Generators besteht darin, Werte einer unendlichen Reihe zu durchlaufen. Hier ein Beispiel für das Finden der ersten zehn Terme der Fibonacci-Sequenz .

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

Unendliche Sequenzen

Generatoren können unendliche Sequenzen darstellen:

def integers_starting_from(n):
    while True:
        yield n
        n += 1

natural_numbers = integers_starting_from(1)

Eine unendliche Zahlenfolge wie oben kann auch mit Hilfe von itertools.count . Der obige Code könnte wie folgt geschrieben werden

natural_numbers = itertools.count(1)

Sie können Generatoren-Verständnis für unendliche Generatoren verwenden, um neue Generatoren herzustellen:

multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)

Beachten Sie, dass ein unendlicher Generator kein Ende hat. Wenn Sie ihn also an eine Funktion übergeben, die versucht, den Generator vollständig zu verbrauchen, hat dies schwerwiegende Folgen :

list(multiples_of_two)  # will never terminate, or raise an OS-specific error

Verwenden Sie stattdessen List / Set- xrange mit range (oder xrange für Python <3.0):

first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)] 
# [3, 6, 9, 12, 15]

oder verwenden Sie itertools.islice() , um den Iterator in eine Teilmenge zu schneiden:

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]

Beachten Sie, dass der Originalgenerator ebenso wie alle anderen Generatoren, die von demselben "Root" stammen, aktualisiert wird:

next(natural_numbers)    # yields 16
next(multiples_of_two)   # yields 34
next(multiples_of_four)  # yields 24

Eine unendliche Sequenz kann auch mit einer for Schleife durchlaufen werden . Stellen Sie sicher, dass Sie eine bedingte break Anweisung einfügen, damit die Schleife eventuell beendet wird:

for idx, number in enumerate(multiplies_of_two):
    print(number)
    if idx == 9:
        break  # stop after taking the first 10 multiplies of two

Klassisches Beispiel - Fibonacci-Zahlen

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

Alle Werte aus einem anderen iterierbar

Python 3.x 3.3

Verwenden Sie die yield from wenn Sie alle Werte aus einer anderen Iteration ermitteln wollen:

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]

Dies funktioniert auch mit 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]

Coroutinen

Generatoren können verwendet werden, um Coroutinen zu implementieren:

# 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 werden häufig zum Implementieren von Zustandsmaschinen verwendet, da sie in erster Linie nützlich sind, um Prozeduren mit einer Methode zu erstellen, für die ein Zustand erforderlich ist, um ordnungsgemäß zu funktionieren. Sie bearbeiten einen vorhandenen Zustand und geben den nach Abschluss des Vorgangs erhaltenen Wert zurück.

Rendite mit Rekursion: Alle Dateien in einem Verzeichnis werden rekursiv aufgelistet

Importieren Sie zunächst die Bibliotheken, die mit Dateien arbeiten:

from os import listdir
from os.path import isfile, join, exists

Eine Hilfsfunktion, um nur Dateien aus einem Verzeichnis zu lesen:

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

Eine weitere Hilfsfunktion, um nur die Unterverzeichnisse zu erhalten:

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

Verwenden Sie nun diese Funktionen, um alle Dateien innerhalb eines Verzeichnisses und aller seiner Unterverzeichnisse (mit Generatoren) rekursiv abzurufen:

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

Diese Funktion kann durch den yield from vereinfacht werden:

def get_files_recursive(directory):
    yield from get_files(directory)
    for subdirectory in get_directories(directory):
        yield from get_files_recursive(subdirectory)

Parallele Iteration über Generatoren

Um mehrere Generatoren parallel zu durchlaufen, verwenden Sie den zip eingebauten Code:

for x, y in zip(a,b):
    print(x,y)

Ergebnisse in:

1 x
2 y
3 z

In Python 2 sollten itertools.izip stattdessen itertools.izip verwenden. Hier können wir auch sehen, dass alle zip Funktionen Tupel ergeben.

Beachten Sie, dass zip die Iteration stoppt, sobald einer der iterierbaren Elemente keine Elemente mehr enthält. Wenn Sie so lange iterieren itertools.zip_longest() wie es am längsten durchlaufen wird, verwenden Sie itertools.zip_longest() .

Code zum Erstellen von Listen umgestalten

Angenommen, Sie verfügen über komplexen Code, der eine Liste erstellt und zurückgibt, indem Sie mit einer leeren Liste beginnen und wiederholt an sie anhängen:

def create():
    result = []
    # logic here...
    result.append(value) # possibly in several places
    # more logic...
    return result # possibly in several places

values = create()

Wenn es nicht praktikabel ist, die innere Logik durch ein Listenverständnis zu ersetzen, können Sie die gesamte Funktion in einen Generator vor Ort umwandeln und dann die Ergebnisse sammeln:

def create_gen():
    # logic...
    yield value
    # more logic
    return # not needed if at the end of the function, of course

values = list(create_gen())

Wenn die Logik rekursiv ist, verwenden Sie yield from , um alle Werte des rekursiven Aufrufs in ein "abgeflachtes" Ergebnis aufzunehmen:

def preorder_traversal(node):
    yield node.value
    for child in node.children:
        yield from preorder_traversal(child)

Suchen

Die next Funktion ist auch ohne Iteration nützlich. Durch Übergeben eines Generatorausdrucks an next schnell nach dem ersten Vorkommen eines Elements suchen, das mit einem Prädikat übereinstimmt. Verfahrenscode wie

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)

kann ersetzt werden durch:

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.

Zu diesem Zweck kann es wünschenswert sein, einen Alias ​​wie first = next oder eine Wrapper-Funktion zu erstellen, um die Ausnahme zu konvertieren:

def first(generator):
    try:
        return next(generator)
    except StopIteration:
        raise ValueError


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow