Ist eine Generator-Sprachfunktion wie "Yield" eine gute Idee?


9

PHP, C #, Python und wahrscheinlich einige andere Sprachen haben ein yieldSchlüsselwort, mit dem Generatorfunktionen erstellt werden.

In PHP: http://php.net/manual/en/language.generators.syntax.php

In Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

In C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

Ich bin besorgt, dass als Sprachfeature / yield-einrichtung einige Konventionen verletzt werden. Eines davon ist "Gewissheit". Diese Methode gibt bei jedem Aufruf ein anderes Ergebnis zurück. 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. Wenn Sie also die Generierungsfunktion zufällig aufrufen und ihren vorherigen Status nicht kennen, können Sie nicht erwarten, dass sie ein bestimmtes Ergebnis zurückgibt.

Wie passt eine solche Funktion in das Sprachparadigma? Verstößt es tatsächlich gegen Konventionen? Ist es eine gute Idee, diese Funktion zu haben und zu nutzen? (um ein Beispiel dafür zu geben, was gut und was schlecht ist, gotowar einst ein Merkmal vieler Sprachen und ist es immer noch, aber es wird als schädlich angesehen und als solches aus einigen Sprachen wie Java ausgelöscht). Müssen Programmiersprachen-Compiler / -Interpreter aus Konventionen ausbrechen, um eine solche Funktion zu implementieren? Muss eine Sprache beispielsweise Multithreading implementieren, damit diese Funktion funktioniert, oder kann dies ohne Threading-Technologie durchgeführt werden?


4
yieldist 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.
Robert Harvey

1
In Bezug auf die Qualität der "Sicherheit" ist zu berücksichtigen, dass bei derselben Eingabesequenz eine Reihe von Aufrufen des Iterators genau dieselben Elemente in genau derselben Reihenfolge ergibt.
Robert Harvey

4
Ich bin mir nicht sicher, woher die meisten Ihrer Fragen kommen, da C ++ kein yield Schlüsselwort wie Python hat. Es hat eine statische Methode std::this_thread::yield(), aber das ist kein Schlüsselwort. Das this_threadwürde also fast jedem Aufruf vorangestellt, was ziemlich offensichtlich macht, dass es sich um eine Bibliotheksfunktion handelt, die nur zum Erteilen von Threads dient, und nicht um eine Sprachfunktion zum Erzielen des Kontrollflusses im Allgemeinen.
Ixrec

Link aktualisiert auf C #, einer für C ++ entfernt
Dennis

Antworten:


16

Vorsichtsmaßnahmen zuerst - C # ist die Sprache, die ich am besten kenne, und obwohl sie eine yieldSprache hat yield, die anderen Sprachen sehr ähnlich zu sein scheint , kann es subtile Unterschiede geben, die ich nicht kenne .

Ich bin besorgt, dass der Ertrag als Sprachmerkmal / -einrichtung gegen einige Konventionen verstößt. Eines davon ist "Gewissheit". Diese Methode gibt bei jedem Aufruf ein anderes Ergebnis zurück.

Quatsch. Erwarten oder erwarten Sie wirklich jedes Mal das gleiche Ergebnis, wenn Sie sie anrufen? Wie wäre es mit Restanrufen? Authentifizierung? Artikel aus einer Sammlung holen? Es gibt alle möglichen (guten, nützlichen) Funktionen, die unrein sind.Random.NextConsole.ReadLine

Wie passt eine solche Funktion in das Sprachparadigma? Verstößt es tatsächlich gegen Konventionen?

Ja, yieldspielt wirklich schlecht mit try/catch/finallyund ist nicht erlaubt ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-infinally/ for Mehr Info).

Ist es eine gute Idee, diese Funktion zu haben und zu nutzen?

Es ist sicherlich eine gute Idee, diese Funktion zu haben. Dinge wie C # 's LINQ sind wirklich nett - die träge Auswertung von Sammlungen bietet einen großen Leistungsvorteil und yieldermöglicht, dass solche Dinge in einem Bruchteil des Codes mit einem Bruchteil der Fehler ausgeführt werden, die ein handgerollter Iterator verursachen würde.

Das heißt, es gibt nicht viele Verwendungszwecke für die yieldVerarbeitung von Sammlungen außerhalb des LINQ-Stils. Ich habe es für die Validierungsverarbeitung, die Generierung von Zeitplänen, die Randomisierung und einige andere Dinge verwendet, aber ich gehe davon aus, dass die meisten Entwickler es nie verwendet (oder missbraucht) haben.

Müssen Programmiersprachen-Compiler / -Interpreter aus Konventionen ausbrechen, um eine solche Funktion zu implementieren? Muss eine Sprache beispielsweise Multithreading implementieren, damit diese Funktion funktioniert, oder kann dies ohne Threading-Technologie durchgeführt werden?

Nicht genau. Der Compiler generiert einen Zustandsmaschinen-Iterator, der verfolgt, wo er gestoppt wurde, damit er beim nächsten Aufruf dort erneut starten kann. Der Prozess für die Codegenerierung ähnelt dem Continuation Passing Style, bei dem der Code nach dem yieldin einen eigenen Block gezogen wird (und wenn er einen yields hat, einen anderen Unterblock usw.). Dies ist ein bekannter Ansatz, der in der funktionalen Programmierung häufiger verwendet wird und auch in der asynchronen / wartenden Kompilierung von C # angezeigt wird.

Es ist kein Threading erforderlich, erfordert jedoch bei den meisten Compilern einen anderen Ansatz zur Codegenerierung und weist Konflikte mit anderen Sprachfunktionen auf.

Alles in allem handelt es sich jedoch yieldum eine Funktion mit relativ geringen Auswirkungen, die bei einer bestimmten Teilmenge von Problemen wirklich hilfreich ist.


Ich habe C # noch nie ernsthaft verwendet, aber dieses yieldSchlüsselwort ähnelt Coroutinen, ja, oder etwas anderem? Wenn ja, wünschte ich, ich hätte eine in C! Ich kann mir zumindest einige anständige Codeabschnitte vorstellen, die mit einer solchen Sprachfunktion viel einfacher zu schreiben gewesen wären.

2
@ DrunkCoder - ähnlich, aber mit einigen Einschränkungen, wie ich es verstehe.
Telastyn

1
Sie möchten auch nicht, dass der Ertrag missbraucht wird. Je mehr Funktionen eine Sprache hat, desto wahrscheinlicher ist es, dass Sie ein Programm finden, das schlecht in dieser Sprache geschrieben ist. Ich bin mir nicht sicher, ob der richtige Ansatz zum Schreiben einer ansprechbaren Sprache darin besteht, alles auf dich zu werfen und zu sehen, was bleibt.
Neil

1
@ DrunkCoder: Es ist eine limitierte Version von Semi-Coroutinen. Tatsächlich wird es vom Compiler als syntaktisches Muster behandelt, das zu einer Reihe von Methodenaufrufen, Klassen und Objekten erweitert wird. (Grundsätzlich generiert der Compiler ein Fortsetzungsobjekt, das den aktuellen Kontext in Feldern erfasst.) Die Standardimplementierung für Sammlungen ist eine Halbkoroutine. Durch Überladen der vom Compiler verwendeten "magischen" Methoden können Sie das Verhalten jedoch tatsächlich anpassen. Zum Beispiel, bevor async/ awaitder Sprache hinzugefügt wurde, hat es jemand mit implementiert yield.
Jörg W Mittag

1
@Neil Es ist im Allgemeinen möglich, praktisch jede Programmiersprachenfunktion zu missbrauchen. Wenn das, was Sie sagen, wahr wäre, wäre es viel schwieriger, mit C schlecht zu programmieren als mit Python oder C #, aber dies ist nicht der Fall, da diese Sprachen viele Tools haben, die Programmierer vor vielen der Fehler schützen, die sehr einfach sind mit C. zu machen In Wirklichkeit sind schlechte Programmierer die Ursache für schlechte Programme - es ist ein ziemlich sprachunabhängiges Problem.
Ben Cottrell

12

Ist eine Generator-Spracheinrichtung yieldeine 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 yieldSchlü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 sumAkzeptieren 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.


6

Wenn Sie an klassische OOP-Sprachen und Generatoren gewöhnt sind, yieldkann dies zu Problemen führen, da der veränderbare Status eher auf Funktionsebene als auf Objektebene erfasst wird.

Die Frage der "Gewissheit" ist jedoch ein roter Hering. Es wird normalerweise als referenzielle Transparenz bezeichnet und bedeutet im Grunde, dass die Funktion immer das gleiche Ergebnis für die gleichen Argumente zurückgibt. Sobald Sie einen veränderlichen Status haben, verlieren Sie die referenzielle Transparenz. In OOP haben Objekte häufig einen veränderlichen Status, was bedeutet, dass das Ergebnis eines Methodenaufrufs nicht nur von den Argumenten abhängt, sondern auch vom internen Status des Objekts.

Die Frage ist, wo der veränderliche Zustand erfasst werden soll. In einem klassischen OOP existiert ein veränderlicher Zustand auf Objektebene. Wenn eine Sprachunterstützung geschlossen wird, haben Sie möglicherweise einen veränderlichen Status auf Funktionsebene. Zum Beispiel in JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Kurz gesagt, yieldist natürlich in einer Sprache, die Schließungen unterstützt, aber in einer Sprache wie der älteren Version von Java, in der der veränderbare Status nur auf Objektebene existiert , fehl am Platz .


Ich nehme an, wenn Sprachmerkmale ein Spektrum hätten, wäre der Ertrag so weit wie möglich von der Funktionsfähigkeit entfernt. Das ist nicht unbedingt eine schlechte Sache. OOP war einst sehr modisch und später wieder funktionale Programmierung. Ich nehme an, die Gefahr besteht darin, Funktionen wie Yield mit einem funktionalen Design zu mischen und abzugleichen, das Ihr Programm auf unerwartete Weise verhalten lässt.
Neil

0

Meiner Meinung nach ist es keine gute Funktion. Es ist eine schlechte Eigenschaft, vor allem, weil es sehr sorgfältig unterrichtet werden muss und jeder es falsch lehrt. Menschen verwenden das Wort "Generator", das zwischen der Generatorfunktion und dem Generatorobjekt nicht eindeutig ist. Die Frage ist: Nur wer oder was macht den tatsächlichen Ertrag?

Dies ist nicht nur meine Meinung. Sogar Guido gibt in dem PEP-Bulletin, in dem er darüber entscheidet, zu, dass die Generatorfunktion kein Generator, sondern eine "Generatorfabrik" ist.

Das ist irgendwie wichtig, findest du nicht? Wenn Sie jedoch 99% der Dokumentation lesen, haben Sie den Eindruck, dass die Generatorfunktion der eigentliche Generator ist, und sie ignorieren tendenziell die Tatsache, dass Sie auch ein Generatorobjekt benötigen.

Guido überlegte, "def" durch "gen" für diese Funktionen zu ersetzen und sagte Nein. Aber ich würde argumentieren, dass das sowieso nicht genug gewesen wäre. Es sollte wirklich sein:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.