Ist eine Generator-Spracheinrichtung yield
eine gute Idee?
Ich möchte dies aus Python-Sicht mit einem nachdrücklichen Ja beantworten , es ist eine großartige Idee .
Ich werde zunächst einige Fragen und Annahmen in Ihrer Frage ansprechen und später die Verbreitung von Generatoren und ihre unangemessene Nützlichkeit in Python demonstrieren.
Mit einer regulären Nicht-Generator-Funktion können Sie sie aufrufen. Wenn sie denselben Eingang erhält, gibt sie denselben Ausgang zurück. Mit Yield gibt es je nach internem Status unterschiedliche Ausgaben zurück.
Das ist falsch. Methoden an Objekten können als Funktionen selbst mit ihrem eigenen internen Zustand betrachtet werden. In Python können Sie, da alles ein Objekt ist, tatsächlich eine Methode von einem Objekt abrufen und diese Methode weitergeben (die an das Objekt gebunden ist, von dem sie stammt, sodass sie sich an ihren Status erinnert).
Andere Beispiele umfassen absichtlich zufällige Funktionen sowie Eingabemethoden wie das Netzwerk, das Dateisystem und das Terminal.
Wie passt eine solche Funktion in das Sprachparadigma?
Wenn das Sprachparadigma beispielsweise erstklassige Funktionen unterstützt und die Generatoren andere Sprachfunktionen wie das Iterable-Protokoll unterstützen, fügen sie sich nahtlos ein.
Verstößt es tatsächlich gegen Konventionen?
Nein. Da es in die Sprache eingebettet ist, basieren die Konventionen auf der Verwendung von Generatoren (oder erfordern diese!).
Müssen Compiler / Interpreter von Programmiersprachen aus Konventionen ausbrechen, um eine solche Funktion zu implementieren?
Wie bei jeder anderen Funktion muss der Compiler lediglich so konzipiert sein, dass er die Funktion unterstützt. Im Fall von Python sind Funktionen bereits Objekte mit Status (wie die Standardargumente und Funktionsanmerkungen).
Muss eine Sprache Multithreading implementieren, damit diese Funktion funktioniert, oder kann dies ohne Threading-Technologie erfolgen?
Unterhaltsame Tatsache: Die Standard-Python-Implementierung unterstützt das Threading überhaupt nicht. Es verfügt über eine globale Interpreter-Sperre (GIL), sodass nichts gleichzeitig ausgeführt wird, es sei denn, Sie haben einen zweiten Prozess gestartet, um eine andere Instanz von Python auszuführen.
Hinweis: Beispiele finden Sie in Python 3
Jenseits der Ausbeute
Während das yield
Schlüsselwort in jeder Funktion verwendet werden kann, um daraus einen Generator zu machen, ist dies nicht die einzige Möglichkeit, einen zu erstellen. Python bietet Generator-Ausdrücke, eine leistungsstarke Methode, um einen Generator in Bezug auf eine andere Iteration (einschließlich anderer Generatoren) klar auszudrücken.
>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155
Wie Sie sehen können, ist nicht nur die Syntax sauber und lesbar, sondern auch integrierte Funktionen wie sum
Akzeptieren von Generatoren.
Mit
Überprüfen Sie den Python-Erweiterungsvorschlag für die With-Anweisung . Es ist ganz anders, als Sie es von einer With-Anweisung in anderen Sprachen erwarten. Mit ein wenig Hilfe aus der Standardbibliothek arbeiten Pythons Generatoren hervorragend als Kontextmanager für sie.
>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
print("preprocessing", arg)
yield arg
print("postprocessing", arg)
>>> with debugWith("foobar") as s:
print(s[::-1])
preprocessing foobar
raboof
postprocessing foobar
Natürlich ist das Drucken das Langweiligste, was Sie hier tun können, aber es zeigt sichtbare Ergebnisse. Weitere interessante Optionen sind die automatische Verwaltung von Ressourcen (Öffnen und Schließen von Dateien / Streams / Netzwerkverbindungen), das Sperren für Parallelität, das vorübergehende Umschließen oder Ersetzen einer Funktion sowie das Dekomprimieren und erneute Komprimieren von Daten. Wenn das Aufrufen von Funktionen dem Einfügen von Code in Ihren Code gleicht, entspricht das Aufrufen von Anweisungen dem Umschließen von Teilen Ihres Codes in anderen Code. Wie auch immer Sie es verwenden, es ist ein solides Beispiel für einen einfachen Einstieg in eine Sprachstruktur. Ertragsbasierte Generatoren sind nicht die einzige Möglichkeit, Kontextmanager zu erstellen, aber sie sind sicherlich eine bequeme.
Für und teilweise Erschöpfung
For-Schleifen in Python funktionieren auf interessante Weise. Sie haben das folgende Format:
for <name> in <iterable>:
...
Zuerst wird der Ausdruck, den ich aufgerufen habe, <iterable>
ausgewertet, um ein iterierbares Objekt zu erhalten. Zweitens hat das iterable es __iter__
aufgerufen, und der resultierende Iterator wird hinter den Kulissen gespeichert. Anschließend __next__
wird der Iterator aufgerufen, um einen Wert zu erhalten, der an den von Ihnen eingegebenen Namen gebunden werden soll <name>
. Dieser Schritt wird wiederholt, bis der Aufruf zum __next__
Auslösen von a StopIteration
. Die Ausnahme wird von der for-Schleife verschluckt und die Ausführung von dort aus fortgesetzt.
Zurück zu den Generatoren: Wenn Sie __iter__
einen Generator aufrufen , gibt er sich einfach selbst zurück.
>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272
Dies bedeutet, dass Sie die Iteration über etwas von dem trennen können, was Sie damit tun möchten, und dieses Verhalten auf halbem Weg ändern können. Beachten Sie unten, wie derselbe Generator in zwei Schleifen verwendet wird und in der zweiten beginnt er dort auszuführen, wo er von der ersten aufgehört hat.
>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
print(ord(letter))
if letter > 'p':
break
109
111
114
>>> for letter in generator:
print(letter)
e
b
o
r
i
n
g
s
t
u
f
f
Faule Bewertung
Eine der Nachteile von Generatoren im Vergleich zu Listen ist das einzige, worauf Sie in einem Generator zugreifen können, das nächste, was dabei herauskommt. Sie können nicht zurückgehen und wie bei einem vorherigen Ergebnis oder zu einem späteren springen, ohne die Zwischenergebnisse durchzugehen. Die Kehrseite davon ist, dass ein Generator im Vergleich zu seiner entsprechenden Liste fast keinen Speicher belegen kann.
>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
sys.getsizeof([x for x in range(10000000000)])
File "<pyshell#10>", line 1, in <listcomp>
sys.getsizeof([x for x in range(10000000000)])
MemoryError
Generatoren können auch träge verkettet werden.
logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))
Die erste, zweite und dritte Zeile definieren jeweils nur einen Generator, erledigen aber keine wirkliche Arbeit. Wenn die letzte Zeile aufgerufen wird, fragt sum numericcolumn nach einem Wert, numericcolumn benötigt einen Wert aus lastcolumn, lastcolumn fragt nach einem Wert aus logfile, der dann tatsächlich eine Zeile aus der Datei liest. Dieser Stapel wird abgewickelt, bis die Summe ihre erste Ganzzahl erhält. Dann wird der Vorgang für die zweite Zeile erneut ausgeführt. Zu diesem Zeitpunkt hat die Summe zwei Ganzzahlen und addiert sie. Beachten Sie, dass die dritte Zeile noch nicht aus der Datei gelesen wurde. Die Summe fordert dann weiterhin Werte von der numerischen Spalte an (ohne den Rest der Kette zu beachten) und fügt sie hinzu, bis die numerische Spalte erschöpft ist.
Der wirklich interessante Teil hier ist, dass die Zeilen einzeln gelesen, verbraucht und verworfen werden. Zu keinem Zeitpunkt befindet sich die gesamte Datei auf einmal im Speicher. Was passiert, wenn diese Protokolldatei beispielsweise ein Terabyte ist? Es funktioniert nur, weil es jeweils nur eine Zeile liest.
Fazit
Dies ist keine vollständige Überprüfung aller Verwendungen von Generatoren in Python. Insbesondere habe ich unendliche Generatoren, Zustandsautomaten, die Rückgabe von Werten und deren Beziehung zu Coroutinen übersprungen.
Ich glaube, es reicht aus, um zu demonstrieren, dass Sie Generatoren als sauber integrierte, nützliche Sprachfunktion haben können.
yield
ist im Wesentlichen eine staatliche Engine. Es ist nicht beabsichtigt, jedes Mal das gleiche Ergebnis zurückzugeben. Was es wird mit absoluter Sicherheit zu tun ist das nächste Element in einem enumerable Rückkehr jedes Mal aufgerufen wird. Threads sind nicht erforderlich; Sie benötigen eine Schließung (mehr oder weniger), um den aktuellen Status beizubehalten.