Einführung
In diesem Lab erfahren Sie, wie Sie die Iteration in Python mithilfe von Generatoren anpassen können. Sie werden auch die Iterator-Funktionalität in benutzerdefinierten Klassen implementieren und Generatoren für Streaming-Datenquellen erstellen.
Die Datei structure.py wird modifiziert, und während des Experiments wird eine neue Datei namens follow.py erstellt.
Grundlagen zu Python-Generatoren
Generatoren sind eine leistungsstarke Funktion in Python. Sie bieten eine einfache und elegante Möglichkeit, Iteratoren zu erstellen. In Python sind Iteratoren bei der Arbeit mit Datenfolgen sehr nützlich, da sie es ermöglichen, schrittweise durch eine Reihe von Werten zu iterieren. Normale Funktionen geben in der Regel einen einzelnen Wert zurück und beenden dann die Ausführung. Generatoren hingegen können über einen Zeitraum hinweg eine Folge von Werten liefern, was bedeutet, dass sie schrittweise mehrere Werte produzieren können.
Was ist ein Generator?
Eine Generatorfunktion sieht ähnlich aus wie eine normale Funktion. Der entscheidende Unterschied liegt jedoch in der Art, wie sie Werte zurückgibt. Anstatt die return-Anweisung zu verwenden, um ein einzelnes Ergebnis bereitzustellen, verwendet eine Generatorfunktion die yield-Anweisung. Die yield-Anweisung ist speziell. Jedes Mal, wenn sie ausgeführt wird, wird der Zustand der Funktion angehalten, und der Wert, der auf das yield-Schlüsselwort folgt, wird an den Aufrufer zurückgegeben. Wenn die Generatorfunktion erneut aufgerufen wird, wird die Ausführung genau dort fortgesetzt, wo sie aufgehört hat.
Beginnen wir mit der Erstellung einer einfachen Generatorfunktion. Die integrierte range()-Funktion in Python unterstützt keine Bruchschritte. Wir werden daher eine Generatorfunktion erstellen, die eine Zahlenfolge mit einem Bruchschritt erzeugen kann.
- Öffnen Sie zunächst ein neues Python-Terminal in der WebIDE. Klicken Sie dazu auf das Menü "Terminal" und wählen Sie dann "Neues Terminal".
- Sobald das Terminal geöffnet ist, geben Sie den folgenden Code in das Terminal ein. Dieser Code definiert eine Generatorfunktion und testet sie anschließend.
def frange(start, stop, step):
current = start
while current < stop:
yield current
current += step
## Test the generator with a for loop
for x in frange(0, 2, 0.25):
print(x, end=' ')
In diesem Code ist die frange-Funktion eine Generatorfunktion. Sie initialisiert eine Variable current mit dem start-Wert. Solange current kleiner als der stop-Wert ist, gibt sie den current-Wert zurück und erhöht dann current um den step-Wert. Die for-Schleife iteriert dann über die von der frange-Generatorfunktion erzeugten Werte und gibt sie aus.
Sie sollten die folgende Ausgabe sehen:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75
Die einmalige Natur von Generatoren
Eine wichtige Eigenschaft von Generatoren ist, dass sie erschöpfbar sind. Dies bedeutet, dass Sie, nachdem Sie alle von einem Generator erzeugten Werte durchlaufen haben, ihn nicht erneut verwenden können, um dieselbe Wertesequenz zu erzeugen. Wir demonstrieren dies anhand des folgenden Codes:
## Create a generator object
f = frange(0, 2, 0.25)
## First iteration works fine
print("First iteration:")
for x in f:
print(x, end=' ')
print("\n")
## Second iteration produces nothing
print("Second iteration:")
for x in f:
print(x, end=' ')
print("\n")
In diesem Code erstellen wir zunächst ein Generatorobjekt f mithilfe der frange-Funktion. Die erste for-Schleife iteriert über alle vom Generator erzeugten Werte und gibt sie aus. Nach der ersten Iteration ist der Generator erschöpft, was bedeutet, dass er bereits alle Werte erzeugt hat, die er kann. Wenn wir also versuchen, ihn in der zweiten for-Schleife erneut zu durchlaufen, erzeugt er keine neuen Werte.
Ausgabe:
First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75
Second iteration:
Beachten Sie, dass die zweite Iteration keine Ausgabe erzeugt hat, da der Generator bereits erschöpft war.
Erstellen wiederverwendbarer Generatoren mit Klassen
Wenn Sie mehrmals über dieselbe Wertesequenz iterieren müssen, können Sie den Generator in einer Klasse verpacken. Auf diese Weise wird jedes Mal, wenn Sie eine neue Iteration starten, ein neuer Generator erstellt.
class FRange:
def __init__(self, start, stop, step):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
n = self.start
while n < self.stop:
yield n
n += self.step
## Create an instance
f = FRange(0, 2, 0.25)
## We can iterate multiple times
print("First iteration:")
for x in f:
print(x, end=' ')
print("\n")
print("Second iteration:")
for x in f:
print(x, end=' ')
print("\n")
In diesem Code definieren wir eine Klasse FRange. Die __init__-Methode initialisiert die start-, stop- und step-Werte. Die __iter__-Methode ist eine spezielle Methode in Python-Klassen. Sie wird verwendet, um einen Iterator zu erstellen. Innerhalb der __iter__-Methode haben wir einen Generator, der Werte auf ähnliche Weise erzeugt wie die zuvor definierte frange-Funktion.
Wenn wir eine Instanz f der FRange-Klasse erstellen und mehrmals über sie iterieren, ruft jede Iteration die __iter__-Methode auf, die einen neuen Generator erstellt. So können wir dieselbe Wertesequenz mehrmals erhalten.
Ausgabe:
First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75
Second iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75
Diesmal können wir mehrmals iterieren, da die __iter__()-Methode jedes Mal, wenn sie aufgerufen wird, einen neuen Generator erstellt.
Hinzufügen von Iterationsfähigkeiten zu benutzerdefinierten Klassen
Nachdem Sie die Grundlagen von Generatoren verstanden haben, werden wir diese nutzen, um Iterationsfähigkeiten zu benutzerdefinierten Klassen hinzuzufügen. In Python müssen Sie, wenn Sie eine Klasse iterierbar machen möchten, die spezielle Methode __iter__() implementieren. Eine iterierbare Klasse ermöglicht es Ihnen, durch ihre Elemente zu iterieren, ähnlich wie Sie durch eine Liste oder ein Tupel iterieren können. Dies ist eine leistungsstarke Funktion, die Ihre benutzerdefinierten Klassen flexibler und einfacher zu handhaben macht.
Grundlagen zur __iter__()-Methode
Die __iter__()-Methode ist ein entscheidender Bestandteil, um eine Klasse iterierbar zu machen. Sie sollte ein Iteratorobjekt zurückgeben. Ein Iterator ist ein Objekt, über das iteriert (in einer Schleife) werden kann. Ein einfacher und effektiver Weg, dies zu erreichen, ist die Definition von __iter__() als Generatorfunktion. Eine Generatorfunktion verwendet das yield-Schlüsselwort, um nacheinander eine Folge von Werten zu produzieren. Jedes Mal, wenn die yield-Anweisung erreicht wird, wird die Funktion angehalten und der Wert zurückgegeben. Beim nächsten Aufruf des Iterators wird die Funktion dort fortgesetzt, wo sie aufgehört hat.
Modifizieren der Structure-Klasse
In der Einrichtung für dieses Lab haben wir eine Basis-Structure-Klasse bereitgestellt. Andere Klassen, wie Stock, können von dieser Structure-Klasse erben. Vererbung ist eine Möglichkeit, eine neue Klasse zu erstellen, die die Eigenschaften und Methoden einer bestehenden Klasse erbt. Indem wir der Structure-Klasse eine __iter__()-Methode hinzufügen, können wir alle ihre Unterklassen iterierbar machen. Dies bedeutet, dass jede Klasse, die von Structure erbt, automatisch die Fähigkeit hat, in einer Schleife durchlaufen zu werden.
- Öffnen Sie die Datei
structure.pyin der WebIDE:
cd ~/project
Dieser Befehl wechselt das aktuelle Arbeitsverzeichnis in das project-Verzeichnis, in dem sich die Datei structure.py befindet. Sie müssen sich im richtigen Verzeichnis befinden, um die Datei zugreifen und modifizieren zu können.
- Betrachten Sie die aktuelle Implementierung der
Structure-Klasse:
class Structure(metaclass=StructureMeta):
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
for name, val in zip(self._fields, args):
setattr(self, '_'+name, val)
Die Structure-Klasse hat eine _fields-Liste, die die Namen der Attribute speichert. Die __init__()-Methode ist der Konstruktor der Klasse. Sie initialisiert die Attribute des Objekts, indem sie überprüft, ob die Anzahl der übergebenen Argumente gleich der Anzahl der Felder ist. Wenn nicht, wird ein TypeError ausgelöst. Andernfalls werden die Attribute mit der setattr()-Funktion festgelegt.
- Fügen Sie eine
__iter__()-Methode hinzu, die nacheinander jeden Attributwert zurückgibt:
def __iter__(self):
for name in self._fields:
yield getattr(self, name)
Diese __iter__()-Methode ist eine Generatorfunktion. Sie durchläuft die _fields-Liste und verwendet die getattr()-Funktion, um den Wert jedes Attributs zu erhalten. Das yield-Schlüsselwort gibt dann die Werte nacheinander zurück.
Die vollständige structure.py-Datei sollte jetzt wie folgt aussehen:
class StructureMeta(type):
def __new__(cls, name, bases, clsdict):
fields = clsdict.get('_fields', [])
for name in fields:
clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
return super().__new__(cls, name, bases, clsdict)
class Structure(metaclass=StructureMeta):
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
for name, val in zip(self._fields, args):
setattr(self, '_'+name, val)
def __iter__(self):
for name in self._fields:
yield getattr(self, name)
Diese aktualisierte Structure-Klasse hat jetzt die __iter__()-Methode, die sie und ihre Unterklassen iterierbar macht.
Speichern Sie die Datei. Nachdem Sie die
structure.py-Datei geändert haben, müssen Sie sie speichern, damit die Änderungen übernommen werden.Testen wir nun die Iterationsfähigkeit, indem wir eine
Stock-Instanz erstellen und über sie iterieren:
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('Iterating over Stock:'); [print(val) for val in s]"
Dieser Befehl erstellt eine Instanz der Stock-Klasse, die von der Structure-Klasse erbt. Anschließend wird über die Instanz mithilfe einer Listen-Komprehension iteriert und jeder Wert ausgegeben.
Sie sollten eine Ausgabe wie diese sehen:
Iterating over Stock:
GOOG
100
490.1
Jede Klasse, die von Structure erbt, ist jetzt automatisch iterierbar, und die Iteration gibt die Attributwerte in der Reihenfolge zurück, die in der _fields-Liste definiert ist. Dies bedeutet, dass Sie problemlos durch die Attribute jeder Unterklasse von Structure iterieren können, ohne zusätzlichen Code für die Iteration schreiben zu müssen.
Verbessern von Klassen mit Iterationsfähigkeiten
Jetzt haben wir unsere Structure-Klasse und ihre Unterklassen so erweitert, dass sie Iteration unterstützen. Iteration ist ein leistungsstarkes Konzept in Python, das es Ihnen ermöglicht, schrittweise durch eine Sammlung von Elementen zu iterieren. Wenn eine Klasse Iteration unterstützt, wird sie flexibler und kann mit vielen integrierten Python-Funktionen zusammenarbeiten. Lassen Sie uns untersuchen, wie diese Unterstützung für Iteration viele leistungsstarke Funktionen in Python ermöglicht.
Nutzen von Iteration für Sequenzkonvertierungen
In Python gibt es integrierte Funktionen wie list() und tuple(). Diese Funktionen sind sehr nützlich, da sie jedes iterierbare Objekt als Eingabe akzeptieren können. Ein iterierbares Objekt ist etwas, über das Sie in einer Schleife iterieren können, wie eine Liste, ein Tupel oder jetzt auch Instanzen unserer Structure-Klasse. Da unsere Structure-Klasse jetzt Iteration unterstützt, können wir Instanzen dieser Klasse problemlos in Listen oder Tupel umwandeln.
- Versuchen wir diese Operationen mit einer
Stock-Instanz. DieStock-Klasse ist eine Unterklasse vonStructure. Führen Sie den folgenden Befehl in Ihrem Terminal aus:
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('As list:', list(s)); print('As tuple:', tuple(s))"
Dieser Befehl importiert zunächst die Stock-Klasse, erstellt eine Instanz davon und wandelt diese Instanz dann mithilfe der list()- und tuple()-Funktionen in eine Liste und ein Tupel um. Die Ausgabe zeigt Ihnen die Instanz als Liste und als Tupel:
As list: ['GOOG', 100, 490.1]
As tuple: ('GOOG', 100, 490.1)
Entpacken (Unpacking)
Python hat eine sehr nützliche Funktion namens Entpacken (Unpacking). Entpacken ermöglicht es Ihnen, ein iterierbares Objekt zu nehmen und seine Elemente auf einmal einzelnen Variablen zuzuweisen. Da unsere Stock-Instanz iterierbar ist, können wir diese Entpackungsfunktion darauf anwenden.
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); name, shares, price = s; print(f'Name: {name}, Shares: {shares}, Price: {price}')"
In diesem Code erstellen wir eine Stock-Instanz und entpacken dann ihre Elemente in drei Variablen: name, shares und price. Anschließend geben wir diese Variablen aus. Die Ausgabe zeigt die Werte dieser Variablen:
Name: GOOG, Shares: 100, Price: 490.1
Hinzufügen von Vergleichsfähigkeiten
Wenn eine Klasse Iteration unterstützt, wird es einfacher, Vergleichsoperationen zu implementieren. Vergleichsoperationen werden verwendet, um zu prüfen, ob zwei Objekte gleich sind oder nicht. Fügen wir unserer Structure-Klasse eine __eq__()-Methode hinzu, um Instanzen zu vergleichen.
- Öffnen Sie die Datei
structure.pyerneut. Die__eq__()-Methode ist eine spezielle Methode in Python, die aufgerufen wird, wenn Sie den==-Operator verwenden, um zwei Objekte zu vergleichen. Fügen Sie den folgenden Code in dieStructure-Klasse in der Dateistructure.pyein:
def __eq__(self, other):
return isinstance(other, type(self)) and tuple(self) == tuple(other)
Diese Methode prüft zunächst, ob das other-Objekt eine Instanz derselben Klasse wie self ist, indem sie die isinstance()-Funktion verwendet. Dann werden sowohl self als auch other in Tupel umgewandelt und geprüft, ob diese Tupel gleich sind.
Die vollständige structure.py-Datei sollte jetzt wie folgt aussehen:
class StructureMeta(type):
def __new__(cls, name, bases, clsdict):
fields = clsdict.get('_fields', [])
for name in fields:
clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
return super().__new__(cls, name, bases, clsdict)
class Structure(metaclass=StructureMeta):
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
for name, val in zip(self._fields, args):
setattr(self, '_'+name, val)
def __iter__(self):
for name in self._fields:
yield getattr(self, name)
def __eq__(self, other):
return isinstance(other, type(self)) and tuple(self) == tuple(other)
Nachdem Sie die
__eq__()-Methode hinzugefügt haben, speichern Sie die Dateistructure.py.Testen wir die Vergleichsfähigkeit. Führen Sie den folgenden Befehl in Ihrem Terminal aus:
python3 -c "from stock import Stock; a = Stock('GOOG', 100, 490.1); b = Stock('GOOG', 100, 490.1); c = Stock('AAPL', 200, 123.4); print(f'a == b: {a == b}'); print(f'a == c: {a == c}')"
Dieser Code erstellt drei Stock-Instanzen: a, b und c. Dann vergleicht er a mit b und a mit c mithilfe des ==-Operators. Die Ausgabe zeigt die Ergebnisse dieser Vergleiche:
a == b: True
a == c: False
- Um sicherzustellen, dass alles korrekt funktioniert, müssen wir die Unittests ausführen. Unittests sind eine Reihe von Code, die prüfen, ob verschiedene Teile Ihres Programms wie erwartet funktionieren. Führen Sie den folgenden Befehl in Ihrem Terminal aus:
python3 teststock.py
Wenn alles korrekt funktioniert, sollten Sie eine Ausgabe sehen, die anzeigt, dass die Tests bestanden wurden:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Indem wir nur zwei einfache Methoden (__iter__() und __eq__()) hinzugefügt haben, haben wir unsere Structure-Klasse erheblich verbessert und sie so gemacht, dass sie mehr Python-typisch und einfacher zu verwenden ist.
Erstellen eines Generators für Streaming-Daten
In der Programmierung sind Generatoren ein leistungsstarkes Werkzeug, insbesondere wenn es um reale Probleme wie die Überwachung einer Streaming-Datenquelle geht. In diesem Abschnitt lernen wir, wie wir das, was wir über Generatoren gelernt haben, auf ein solches praktisches Szenario anwenden können. Wir werden einen Generator erstellen, der eine Protokolldatei (Log-Datei) überwacht und uns neue Zeilen liefert, sobald sie zur Datei hinzugefügt werden.
Einrichten der Datenquelle
Bevor wir mit der Erstellung des Generators beginnen, müssen wir eine Datenquelle einrichten. In diesem Fall verwenden wir ein Simulationsprogramm, das Börsendaten generiert.
Zunächst müssen Sie ein neues Terminal in der WebIDE öffnen. Hier werden Sie die Befehle ausführen, um die Simulation zu starten.
Nachdem Sie das Terminal geöffnet haben, führen Sie das Börsensimulationsprogramm aus. Hier sind die Befehle, die Sie eingeben müssen:
cd ~/project
python3 stocksim.py
Der erste Befehl cd ~/project wechselt das aktuelle Verzeichnis in das project-Verzeichnis in Ihrem Home-Verzeichnis. Der zweite Befehl python3 stocksim.py führt das Börsensimulationsprogramm aus. Dieses Programm generiert Börsendaten und schreibt sie in eine Datei namens stocklog.csv im aktuellen Verzeichnis. Lassen Sie dieses Programm im Hintergrund laufen, während wir an dem Überwachungscode arbeiten.
Erstellen eines einfachen Datei-Überwachers
Jetzt, da wir unsere Datenquelle eingerichtet haben, erstellen wir ein Programm, das die Datei stocklog.csv überwacht. Dieses Programm zeigt alle negativen Preisänderungen an.
- Erstellen Sie zunächst eine neue Datei namens
follow.pyin der WebIDE. Dazu müssen Sie das Verzeichnis in dasproject-Verzeichnis wechseln, indem Sie den folgenden Befehl im Terminal eingeben:
cd ~/project
- Fügen Sie als Nächstes den folgenden Code zur Datei
follow.pyhinzu. Dieser Code öffnet die Dateistocklog.csv, bewegt den Dateizeiger an das Ende der Datei und überprüft dann kontinuierlich auf neue Zeilen. Wenn eine neue Zeile gefunden wird und sie eine negative Preisänderung darstellt, wird der Name der Aktie, der Preis und die Änderung ausgegeben.
## follow.py
import os
import time
f = open('stocklog.csv')
f.seek(0, os.SEEK_END) ## Move file pointer 0 bytes from end of file
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly and retry
continue
fields = line.split(',')
name = fields[0].strip('"')
price = float(fields[1])
change = float(fields[4])
if change < 0:
print('%10s %10.2f %10.2f' % (name, price, change))
- Nachdem Sie den Code hinzugefügt haben, speichern Sie die Datei. Führen Sie dann das Programm aus, indem Sie den folgenden Befehl im Terminal eingeben:
python3 follow.py
Sie sollten eine Ausgabe sehen, die Aktien mit negativen Preisänderungen anzeigt. Es könnte so aussehen:
AAPL 148.24 -1.76
GOOG 2498.45 -1.55
Wenn Sie das Programm beenden möchten, drücken Sie Ctrl+C im Terminal.
Umwandlung in eine Generatorfunktion
Obwohl der vorherige Code funktioniert, können wir ihn wiederverwendbarer und modularer gestalten, indem wir ihn in eine Generatorfunktion umwandeln. Eine Generatorfunktion ist eine spezielle Art von Funktion, die angehalten und fortgesetzt werden kann und die nacheinander Werte liefert.
- Öffnen Sie die Datei
follow.pyerneut und ändern Sie sie so, dass eine Generatorfunktion verwendet wird. Hier ist der aktualisierte Code:
## follow.py
import os
import time
def follow(filename):
"""
Generator function that yields new lines in a file as they are added.
Similar to the 'tail -f' Unix command.
"""
f = open(filename)
f.seek(0, os.SEEK_END) ## Move to the end of the file
while True:
line = f.readline()
if line == '':
time.sleep(0.1) ## Sleep briefly and retry
continue
yield line
## Example usage - monitor stocks with negative price changes
if __name__ == '__main__':
for line in follow('stocklog.csv'):
fields = line.split(',')
name = fields[0].strip('"')
price = float(fields[1])
change = float(fields[4])
if change < 0:
print('%10s %10.2f %10.2f' % (name, price, change))
Die follow-Funktion ist jetzt eine Generatorfunktion. Sie öffnet die Datei, bewegt sich an das Ende und überprüft dann kontinuierlich auf neue Zeilen. Wenn eine neue Zeile gefunden wird, liefert sie diese Zeile.
- Speichern Sie die Datei und führen Sie sie erneut aus, indem Sie den Befehl eingeben:
python3 follow.py
Die Ausgabe sollte die gleiche wie zuvor sein. Aber jetzt ist die Logik zur Dateiüberwachung sauber in der follow-Generatorfunktion gekapselt. Dies bedeutet, dass wir diese Funktion in anderen Programmen wiederverwenden können, die eine Datei überwachen müssen.
Verständnis der Stärke von Generatoren
Durch die Umwandlung unseres Dateilesecodes in eine Generatorfunktion haben wir ihn viel flexibler und wiederverwendbarer gemacht. Die follow()-Funktion kann in jedem Programm verwendet werden, das eine Datei überwachen muss, nicht nur für Börsendaten.
Beispielsweise könnten Sie sie verwenden, um Serverprotokolle, Anwendungslogs oder jede andere Datei zu überwachen, die im Laufe der Zeit aktualisiert wird. Dies zeigt, wie Generatoren eine großartige Möglichkeit sind, Streaming-Datenquellen auf saubere und modulare Weise zu verarbeiten.
Zusammenfassung
In diesem Lab haben Sie gelernt, wie Sie die Iteration in Python mithilfe von Generatoren anpassen können. Sie haben einfache Generatoren mit der yield-Anweisung erstellt, um Wertsequenzen zu generieren, Iterationsunterstützung für benutzerdefinierte Klassen hinzugefügt, indem Sie die __iter__()-Methode implementiert haben, die Iteration für Sequenzkonvertierungen, Entpacken (Unpacking) und Vergleiche genutzt und einen praktischen Generator für die Überwachung einer Streaming-Datenquelle erstellt.
Generatoren sind eine leistungsstarke Python-Funktion, die es Ihnen ermöglicht, Iteratoren mit minimalem Code zu erstellen. Sie sind besonders nützlich für die Verarbeitung großer Datensätze, die Arbeit mit Streaming-Daten, die Erstellung von Datenpipelines und die Implementierung benutzerdefinierter Iterationsmuster. Die Verwendung von Generatoren ermöglicht es Ihnen, saubereren und speicher-effizienteren Code zu schreiben, der Ihre Absicht klar zum Ausdruck bringt.