Python Generators

Table of Contents

In diesem Tutorial erfahren Sie, wie Sie mit Python Generators auf einfache Weise Iterationen erstellen, wie sie sich von Iteratoren und normalen Funktionen unterscheiden und warum Sie sie verwenden sollten.

Generators in Python

 

Es steckt viel Arbeit im Bau und iterator in Python. Wir müssen eine Klasse implementieren mit __iter__() und __next__() Methode, verfolgen die internen Zustände und lösen StopIteration wenn keine Werte zurückgegeben werden sollen.

 

Dies ist sowohl langwierig als auch kontraintuitiv. In solchen Situationen kommt Generator zur Rettung.

Python Generators sind eine einfache Möglichkeit, Iterators zu erstellen. All die oben erwähnten Arbeiten werden in Python automatisch von Generatoren erledigt.

Einfach gesagt ist ein Generator eine Funktion, die ein Objekt (Iterator) zurückgibt, über das wir iterieren können (einen Wert nach dem anderen).

 


 

Create Generators in Python

 

Es ist ziemlich einfach, einen Generator in Python zu erstellen. Es ist so einfach wie das Definieren einer normalen Funktion, aber mit einer yield statement anstelle einer return statement.

 

Enthält eine Funktion mindestens einen yield statement ((sie kann andere yield oder return statements)wird sie zu einer Generator funktion. Beide yield und return gibt einen Wert aus einer Funktion zurück.

 

Der Unterschied besteht darin, dass bei einem return statement beendet eine Funktion vollständig, yield hält die Funktion an, indem sie alle Zustände speichert und später bei weiteren Aufrufen von dort aus fortfährt.

 


 

Differences between Generator function and Normal function

 

Hier ist der Unterschied zwischen einer Generator funktion und einer normalen Funktion.

 

  • Generator funktion enthält eine oder mehrere yield statements.
  • Wenn es aufgerufen wird, gibt es ein Objekt (Iterator) zurück, startet jedoch nicht sofort die Ausführung.
  • Methoden wie __iter__() and __next__() werden automatisch implementiert. So können wir die Elemente mit . iterieren next().
  • Sobald die Funktion nachgibt, wird die Funktion angehalten und die Kontrolle an den Aufrufer übergeben.
  • Lokale Variablen und ihre Zustände werden zwischen aufeinanderfolgenden Aufrufen gespeichert.
  • Schließlich, wenn die Funktion beendet ist StopIteration wird bei weiteren Anrufen automatisch ausgelöst.

 

Hier ist ein Beispiel, um alle oben genannten Punkte zu veranschaulichen. Wir haben eine Generator funktion namens my_gen() mit mehreren yield statements.

# A simple generator function
def my_gen():
    n = 1
    print('Dies wird zuerst gedruckt')
    # Generator Funktion enthält Yield-Anweisungen statements
    yield n

    n += 1
    print('Dies ist die zweite gedruckte')
    yield n

    n += 1
    print('Dies wird endlich gedruckt')
    yield n

 

Ein interaktiver Ablauf im Interpreter ist unten angegeben. Führen Sie diese in der Python-Shell aus, um die Ausgabe anzuzeigen.

>>> # Es gibt ein Objekt zurück, startet jedoch nicht sofort die Ausführung.
>>> a = my_gen()

>>> # Wir können durch die Elemente iterieren mit next().
>>> next(a)
Dies wird zuerst gedruckt
1
>>> # Sobald die Funktion nachgibt, wird die Funktion angehalten und die Kontrolle an den Aufrufer übergeben.

>>> # Lokale Variablen und ihre Zustände werden zwischen aufeinanderfolgenden Aufrufen gespeichert.
>>> next(a)
Dies ist die zweite gedruckte
2

>>> next(a)
This is printed at last
3

>>> # 
Wenn die Funktion schließlich beendet wird, wird StopIteration bei weiteren Aufrufen automatisch ausgelöst.
>>> next(a)
Traceback (most recent call last):
...
StopIteration
>>> next(a)
Traceback (most recent call last):
...
StopIteration

 

Interessant im obigen Beispiel ist, dass der Wert der Variablen n wird zwischen den einzelnen Aufrufen gespeichert.

 

Im Gegensatz zu normalen Funktionen werden die lokalen Variablen nicht zerstört, wenn die Funktion nachgibt. Außerdem kann das Generator objekt nur einmal iteriert werden.

 

Um den Prozess neu zu starten, müssen wir ein weiteres Generator objekt erstellen, indem wir etwas wie a = my_gen().

 

Eine letzte Sache, die zu beachten ist, ist, dass wir Generatoren direkt mit for-Schleifen  verwenden können.

 

Dies liegt daran, dass ein for Schleife nimmt einen Iterator und iteriert über ihn mit next() Funktion. Sie endet automatisch, wenn StopIteration ausgelöst wird. Sehen Sie hier nach, wie eine for-Schleife in Python tatsächlich implementiert ist.

 

# Eine einfache Generator funktion
def my_gen():
    n = 1
    print('Dies wird zuerst gedruckt')
    # Generator funktion enthält yield statements
    yield n

    n += 1
    print('Dies ist die zweite gedruckte')
    yield n

    n += 1
    print('Dies wird endlich gedruckt')
    yield n


# Using for loop
for item in my_gen():
    print(item)

 

Wenn Sie das Programm ausführen, wird die Ausgabe sein

Dies wird zuerst gedruckt
1
Dies ist die zweite gedruckte
2
Dies wird endlich gedruckt
3

 


 

Python Generators with a Loop

 

Das obige Beispiel ist von geringerem Nutzen und wir haben es nur untersucht, um eine Vorstellung davon zu bekommen, was im Hintergrund passiert.

 

Normalerweise werden Generator funktionen mit einer Schleife implementiert, die eine geeignete Abbruchbedingung hat.

Nehmen wir ein Beispiel für einen Generator, der eine Zeichenkette umkehrt.

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)

 

Output

o
l
l
e
h

 

In diesem Beispiel haben wir die range() Funktion, um den Index in umgekehrter Reihenfolge mit Hilfe der for-Schleife zu erhalten.

 

Notiz

Diese Generator funktion arbeitet nicht nur mit Strings, sondern auch mit anderen Arten von Iterablen wie List, Tupel, etc.

 


 

Python Generator Expression

Einfache Generators können mit Hilfe von Generator ausdrücken einfach on-the-fly erstellt werden. Es macht das Erstellen von Generators einfach.

Einfache Generatoren können mit Hilfe von Generatorausdrücken einfach on-the-fly erstellt werden. Es macht das Erstellen von Generatoren einfach.

Ähnlich wie die Lambda-Funktionen, die anonyme Funktionen erzeugen, erzeugen Generatorausdrücke anonyme Generatorfunktionen.

Die Syntax für Generatorausdrücke ist ähnlich wie die einer Listenauffassung in Python. Allerdings werden die eckigen Klammern durch runde Klammern ersetzt.

Der Hauptunterschied zwischen einem Listenaufruf und einem Generatorausdruck ist, dass ein Listenaufruf die gesamte Liste erzeugt, während der Generatorausdruck jeweils ein Element erzeugt.

Sie haben eine träge Ausführung (produzieren Elemente nur, wenn sie angefordert werden). Aus diesem Grund ist ein Generatorausdruck viel speichereffizienter als ein entsprechendes Listenverständnis.

# Initialisieren der Liste
my_list = [1, 3, 6, 10]

# jeden Begriff mit Hilfe des Listenverständnisses quadrieren
list_ = [x**2 for x in my_list]

# Das Gleiche kann mit einem Generator-Ausdruck gemacht werden
# Generatorausdrücke sind von Klammern umgeben ()
generator = (x**2 for x in my_list)

print(list_)
print(generator)

 

Output

[1, 9, 36, 100]
<generator object <genexpr> at 0x7f5d4eb4bf50>

 

Wir können oben sehen, dass der Generator-Ausdruck nicht sofort das gewünschte Ergebnis erzeugt hat. Stattdessen wurde ein Generatorobjekt zurückgegeben, das Elemente nur bei Bedarf produziert.

Hier sehen Sie, wie wir anfangen können, Elemente vom Generator abzurufen:

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

 

Wenn wir das obige Programm ausführen, erhalten wir die folgende Ausgabe:

1
9
36
100
Traceback (most recent call last):
  File "<string>", line 15, in <module>
StopIteration

 

Generator ausdrücke können als Funktionsargumente verwendet werden. Bei einer solchen Verwendung können die runden Klammern entfallen.

>>> sum(x**2 for x in my_list)
146

>>> max(x**2 for x in my_list)
100

 


 

Use of Python Generators

 

Es gibt mehrere Gründe, die Generators zu einer leistungsfähigen Implementierung machen.

1. Einfach zu implementieren

 

Generatorslassen sich im Vergleich zu ihrem Pendant in der Iterator-Klasse übersichtlich und klar implementieren. Es folgt ein Beispiel für die Implementierung einer Folge von 2er-Potenzen mit einer Iterator-Klasse.

 

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

 

 

Das obige Programm war langwierig und verwirrend. Lassen Sie uns nun das Gleiche mit einer Generatorfunktion machen.

def PowTwoGen(max=0):
    n = 0
    while n < max:
        yield 2 ** n
        n += 1

 

Da die Generators die Details automatisch verfolgen, war die Implementierung übersichtlich und viel sauberer.

 

2. Memory Efficient

Eine normale Funktion zur Rückgabe einer Sequenz erstellt die gesamte Sequenz im Speicher, bevor sie das Ergebnis zurückgibt. Dies ist ein Overkill, wenn die Anzahl der Elemente in der Sequenz sehr groß ist.

Die Generator implementierung solcher Sequenzen ist speicherfreundlich und wird bevorzugt, da sie jeweils nur ein Element erzeugt.

 

3. Represent Infinite Stream

 

Generatoren sind ein hervorragendes Medium zur Darstellung eines unendlichen Datenstroms. Unendliche Datenströme können nicht im Speicher gespeichert werden, und da Generatoren jeweils nur ein Element erzeugen, können sie einen unendlichen Datenstrom darstellen.

Die folgende Generator funktion kann alle geraden Zahlen erzeugen (zumindest theoretisch).

def all_even():
    n = 0
    while True:
        yield n
        n += 2

 

4. Pipelining Generators

 

Mehrere Generatorskönnen verwendet werden, um eine Reihe von Operationen über eine Pipeline zu leiten. Dies lässt sich am besten anhand eines Beispiels veranschaulichen.

Angenommen, wir haben einen Generator, der die Zahlen der Fibonacci-Reihe erzeugt. Und wir haben einen weiteren Generator zum Quadrieren von Zahlen.

Wenn wir die Summe der Quadrate der Zahlen in der Fibonacci-Reihe herausfinden wollen, können wir dies auf folgende Weise tun, indem wir die Ausgabe der Generator funktionen zusammen pipelining.

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

 

Output

4895

 

Dieses Pipelining ist effizient und einfach zu lesen.