Beispiele für Realworld Use Cases für Generatoren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

LanX hat geschrieben:Klar für Streams braucht man Iteratoren, aber werden Iteratoren in Python zwangsläufig mit yield konstruiert???
Hättest du mein Post gelesen wüsstest du die Antwort.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

LanX hat geschrieben:Klar für Streams braucht man Iteratoren, aber werden Iteratoren in Python zwangsläufig mit yield konstruiert???
Darii hat doch gezeigt wie das ohne ``yield`` gehen würde…
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

LanX hat geschrieben:Klar für Streams braucht man Iteratoren, aber werden Iteratoren in Python zwangsläufig mit yield konstruiert???
Jein. Du kannst auch eine Klasse schreiben die __iter__ und next implementiert und den State in Attributen speichert, ist aber wesentlich mehr Code und bei weitem nicht so elegant insbesondere dann wenn du auch noch send und throw haben willst.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

DasIch: Man kann auch einen Generatorausdruck schreiben.
LanX
User
Beiträge: 92
Registriert: Samstag 20. Februar 2010, 12:46

Darii hat geschrieben:
LanX hat geschrieben:Klar für Streams braucht man Iteratoren, aber werden Iteratoren in Python zwangsläufig mit yield konstruiert???
Hättest du mein Post gelesen wüsstest du die Antwort.
Ich habe deinen Post gelesen und das Prinzip der rhetorischen Frage ist dir bekannt?

(Vorsicht: die letzte Frage ist rekursiv ;)
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

@LanX
Dann soltest du aber an deiner Rhetorik üben, das konnte man nicht einmal ansatztweise als eine rhetorischen Frage deuten. :)
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

LanX ist der Meinung, dass die offensichtliche Antwort "Nein" lautet. Ich bin aber, wie Xynon1, der Meinung, dass die Antwort eben nicht offensichtlich ist und die Frage daher auch keine rhetorische ist. BTT: noch ein use-case für yield: wenn man z.B. mittels try-except ne Ausnahme abfangen muss, kann man ja auch keine GE verwenden.
crs
User
Beiträge: 42
Registriert: Dienstag 14. Juli 2009, 13:24

LanX hat geschrieben: Schon klar mir geht's aber um das Idiom. Mit Statusvariablen im Closure kann ich eine Funktion auch bereits ohne "yield" zur Continuation machen, um dieses Ziel zu erreichen.

Es ist halt mehr Aufwand.

Ein eigenständiges Idiom hingegen rechtfertigt sich über Häufigkeit der Anwendungsfälle.
Das die Implementierung so mehr Aufwand ist liegt aber vermutlich am Scope von Python. Denn man kann eben nicht sowas schreiben:

Code: Alles auswählen

def generator(x, n):
    y = 0
    def f():
        y += 1
        return (x**y) % n
    return f
Weil Python 2 y in dem Fall als lokale Variable von f sieht (Python 3 kennt nonlocal, damit koennte man das Beispiel implementieren).

Ohne yield koennte man das z.B. mit mit einem zusaetzlichen dictionary als namespace implementieren:

Code: Alles auswählen

def generator(x, n):
    namespace = {'y': 0}
    def f():
        namespace['y'] += 1
        return (x**namespace['y']) % n
    return f
(x und n werden nur lesend verwendet, daher veraendert sich ihr Scope nicht)

Mit yield vereinfacht sich der Code dann etwas, da man den zusaetzlichen namespace nicht braucht:

Code: Alles auswählen

def generator(x, n):
    y = 0
    while True:
        y += 1
        yield (x**y) % n
Statt der Funktion braucht man aber die Schleife.

Generatoren mit yield haben aber den Nachteil das man (afaik) next() nicht mit Parametern aufrufen kann, den accumulator generator kann man also mit yield nicht implementieren?

Code: Alles auswählen

def accgen(n):
    namespace = {'n': n}
    def f(i):
        namespace['n'] += i
        return namespace['n']
    return f
BlackJack

@crs: `next()` nicht, aber `send()`. Allerdings muss man mindestens einmal vorher `next()` aufgerufen haben, damit der Code einmal bis zum `yield` abgearbeitet wurde:

Code: Alles auswählen

def f(n):
    while True:
        x = yield n
        n += x
Testlauf:

Code: Alles auswählen

In [58]: a = f(0)

In [59]: a.next()
Out[59]: 0

In [60]: a.send(42)
Out[60]: 42

In [61]: a.send(23)
Out[61]: 65

In [62]: a.send(1)
Out[62]: 66
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

LanX hat geschrieben:Ich habe deinen Post gelesen und das Prinzip der rhetorischen Frage ist dir bekannt?
Meinst du das jetzt ernst? Aber immerhin bin ich nicht der einzige der dich nicht verstanden hat. ;)
crs hat geschrieben:Generatoren mit yield haben aber den Nachteil das man (afaik) next() nicht mit Parametern aufrufen kann, den accumulator generator kann man also mit yield nicht implementieren?
`next()` wird sowieso nie mit Parametern aufgerufen.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Eher sehr theoretischer Natur, aber du könntest Generatoren, mittels ``yield``, auch für kooperatives Multitasking verwenden. Wenn ich mich richtig entsinne, gab es im Cookbook dazu sogar ein Beispiel.

Sebastian
Das Leben ist wie ein Tennisball.
LanX
User
Beiträge: 92
Registriert: Samstag 20. Februar 2010, 12:46

EyDu hat geschrieben:Eher sehr theoretischer Natur, aber du könntest Generatoren, mittels ``yield``, auch für kooperatives Multitasking verwenden. Wenn ich mich richtig entsinne, gab es im Cookbook dazu sogar ein Beispiel.

Sebastian
Hmm, ich weiß dass Continuations ein Sonderfall von Co-Routinen sind, ich vermute das läuft darauf hinaus.

crs hat geschrieben: Das die Implementierung so mehr Aufwand ist liegt aber vermutlich am Scope von Python. Denn man kann eben nicht sowas schreiben:

Code: Alles auswählen

def generator(x, n):
    y = 0
    def f():
        y += 1
        return (x**y) % n
    return f
Ich merke auch gerade, dass ich etwas zu wenig von Python verstehe, viele Pattern die ich kennen sind nur mit ziemlich viel Aufwand abbildbar.

Wenn man einen Closure als Iterator verwenden will braucht man Unverhältnis viel mehr Code, als wenn man ein Iterator-Objekt nutzt, was aber eine bewusste Designentscheidung von GvR ist, die wir jetzt nicht auszuflamen brauchen.

folgender Perlcode der den deinen um ein Abbruchkriterium erweitert lässt sich IMHO auch fast 1 zu 1 genauso in LISP oder JS abbilden:

Code: Alles auswählen

sub generator {
  my ($x, $n) =  @_;
  my $y = 0;
  return
    sub {
      my ($max) =  @_;
      $y++;
      return   
          $y <= $max   
               ? ($x ** $y ) % $n    
               : () 
    };
}


my $iter = generator(2,5);

while ( my ($val) = $iter->(9) ) { 
  print $val,"\t";
}

# Ausgabe: 4	3	1	2	4	3	1	2
Aber in Python liefert = keinen Rückgabewert, der sich in while auswerten ließe, und für for/in muss es ein Object, sein dass die _iter_() Methode hat.

Sprich man kommt gar nicht darum herum eine Iterator-Objekt zu erzeugen. (oder hab ich hier was verpasst?)

In Perl gibts zwar diverse Iterator-Klassen die man nutzen könnte die Funktionale Lösung ist nur viel schneller als Methodenaufrufe der Art "$iter_obj->next()".

Ich vermute mal die strikte OOP in Python erlaubt hier Optimierungen in den Ausführungszeiten der impliziten Methodenaufrufe in For-in.

EDIT: Initialisierungsfehler korrigiert und wieder zurückgenommen um zum folgecode kompatibel zu bleiben.
Zuletzt geändert von LanX am Samstag 2. April 2011, 19:11, insgesamt 2-mal geändert.
BlackJack

@LanX: ``for`` will grundsätzlich nach dem ``in`` ein "iterable" haben. Wenn Du eine Funktion hast die bei jedem Aufruf einen Wert erzeugt und das Ende durch einen "sentinel"-Wert anzeigt, kannst Du so ein "iterable" mit der `iter()`-Funktion erzeugen. Dein Code so nahe wie möglich am Original in Python:

Code: Alles auswählen

from functools import partial

def generator(x, n):
    y = [0]
    def f(max_):
        y[0] += 1
        return (x**y[0]) % n if y[0] <= max_ else None
    return f

def main():
    iter_func = generator(2, 5)
    for val in iter(partial(iter_func, 9), None):
        print val,

if __name__ == '__main__':
    main()
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ich habe nicht den ganzen Thread gelesen sondern nur das Wort "Continuation". Mit yield (oder sonst irgendwie in Python kann man keine Continuations bauen. Eine Continuation ist eine Funktion, die an einem bestimmten Programmpunkt für den Rest des Programms steht. Wenn ich also "3+4" habe und gerade eben die "3" (von links nach rechts) ausgewertet habe, wäre die Continuation an dieser Stelle die Funktion f(x) = x + 4. In diesem trivialen Beispiel ziemlich unsinnig, aber allgemein interessant, denn weil ich, wenn ich an beliebiger Stelle im Programmablauf mit eine Funktion geben lassen kann, die für den Rest des Programms steht, kann ich mir dieses quasi aufheben und doch etwas anderes machen. Dies wird für Coroutinen benutzt. Diese lassen sich mit Continuations implementieren - aber nicht anders herum. Eine Coroutine ist nur einmal zu durchlaufen, eine Continuation als "echte" Funktion kann beliebig häufig wiederholt werden. Eine noch eingeschränktere Form von Continuation wäre die Escape-Continuation, bei Python try/except und raise genannt. Continuations lassen sich wiederum benutzen, um diese Form ausergewöhnlichen Kontrollflusses zu implementieren - aber nicht anders herum.

Bei Smalltalk oder Dylan (Scheme sowieso und AFAIK auch Common Lisp) sind Exceptions übrigens wiederholbar, d.h. man kann dem System sagen, man möge es an der Stelle, wo abgebrochen wurde, um dann den Exception-Handler auszuführen, doch bitte weitermachen, d.h. in diesem Fall enthalten die Exception-Objekte die Continuation für diese Programmstelle. Total praktisch, weil das nämlich den Debugger erlaubt, in dem man auf einen Fehler laufen kann, diesen dann korrigiert und das Programm weiterlaufen lässt.

Stefan
LanX
User
Beiträge: 92
Registriert: Samstag 20. Februar 2010, 12:46

sma hat geschrieben:Ich habe nicht den ganzen Thread gelesen sondern nur das Wort "Continuation". Mit yield (oder sonst irgendwie in Python kann man keine Continuations bauen. Eine Continuation ist eine Funktion, die an einem bestimmten Programmpunkt für den Rest des Programms steht. ...

Dies wird für Coroutinen benutzt. Diese lassen sich mit Continuations implementieren - aber nicht anders herum. Eine Coroutine ist nur einmal zu durchlaufen, eine Continuation als "echte" Funktion kann beliebig häufig wiederholt werden. Eine noch eingeschränktere Form von Continuation wäre die Escape-Continuation, bei Python try/except und raise genannt. Continuations lassen sich wiederum benutzen, um diese Form ausergewöhnlichen Kontrollflusses zu implementieren - aber nicht anders herum.
Hi

Ich habe tatsächlich Continuation und Coroutine falsch rum in der Erinnerung gehabt, sorry.

Danke! :)

Allerdings werden Python Generatoren in der tat oft mit Coroutinen identifiziert:

Z.B. hier

Python's generator functions are almost coroutines -- but not quite -- in that they allow pausing execution to produce a value, but do not provide for values or exceptions to be passed in when execution resumes.

oder

In Python 2.5 wurde die Syntax des yield-Schlüsselworts erweitert, um die Übergabe von Parametern in die andere Richtung zu ermöglichen.Damit können Koroutinen vollständig in Python implementiert werden

ich habe jetzt ein bisschen recherchiert und es ist wie üblich mühsam sich durch unterschiedliche Umsetzungen diverser Sprachen inkl. verbundenem Begriffswandel und unverifizierter Einträge bei WP zu kämpfen.

Ich besorge mir die Tage den Knuth und schau mir mal an was er dazu geschrieben hat, das Buch ist IMHO alt genug um nahe an den Primärquellen zu sein und anerkannt genug um als Referenz zu dienen.

Tschau
Rolf
Antworten