Leeren Generator zurückgeben (forced).

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.
Antworten
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Hi all,

vermutlich ist dies nur ne dumme Frage, aber leider komm ich nicht ohne weiteres von selbst drauf, nur durch Doku lesen...

Folgendes Szenario:

Nur zum Spaß habe ich versucht, die range() builtin-Funktion selbst in Python zu implementieren und bin darauf auf ein (praktisch irrelevantes) Problem gestoßen.

Code: Alles auswählen

def myrange_gen(stop,start=None,step=None):
    if start is None:
        start = 0
    else:
        # [start,] stop [, step]
        start, stop = stop, start   
    
    if step is None:
        step = 1
    if start is stop:
        return []
    if step == 0:
        return []
    if start < stop and step < 0:
        return []
    if start > stop and step > 0:
        return []

    def inner_myrange_gen(stop,start,step):
        if start < stop:
            while start < stop:
                yield start
                start += step
        else:
            while start > stop:
                yield start
                start += step

    return inner_myrange_gen(stop,start,step)

def myrange(stop,start=None,step=None):
    return list(myrange_gen(stop,start,step))
Ihr seht, ich teile hier auf zwischen myrange() und myrange_gen(), wobei die _gen() Version nur das Generator-Objekt zurückgeben soll,
für den seltenen Fall, dass es vielleicht so gewünscht / bevorzugt wäre, ansonsten ruft man die richtige Version auf, die einem eine fertige Liste zurückgibt.

Ausserdem seht Ihr noch, dass für bestimmte Fälle eine leere Liste zurückgegeben wird. (Die ganzen return [] Zeilen)

Dumme Frage jetzt: angenommen, ich will, dass meine _gen() Funktion aus kosmetischen Gründen (praktisch tut es auch so) IMMER einen Generator zurückgibt,
wie mache ich, dass nen "leeren" Generator zurückgibt? Irgendwie steh ich hier aufm Schlauch, weil ich komm selbst nicht drauf.

Bin wie immer dankbar für eure Antworten und Kommentare.
BlackJack

@akis.kapo: return iter([])
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

BlackJacks Vorschlag ist der bessere, aber es ginge auch so:

Code: Alles auswählen

def nullgen():
    if False:
        yield
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

pillmuncher hat geschrieben:BlackJacks Vorschlag ist der bessere, aber es ginge auch so:

Code: Alles auswählen

def nullgen():
    if False:
        yield
Hmm, jetzt versteh ich auch das if False... wenn du nur yield angibst, gibt er [None] zurück... aber wieso ist BJ's Vorschlag besser?
Deine Funktion gibt ein Generator-Objekt zurück, seine ein listiterator.

Ich dachte da mehr an ein generator([]) oder so... (was es nicht gibt).

D.h. Generator-Objekte kommen ausschliesslich von Funktionen, die ein yield enthalten, korrekt?
Oder gibts doch noch weitere Möglichkeiten, die zu erzeugen?
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

akis.kapo hat geschrieben:aber wieso ist BJ's Vorschlag besser?
Deine Funktion gibt ein Generator-Objekt zurück, seine ein listiterator.
Kürzer, leserlicher und der Effekt ist sofort ersichtlich.
akis.kapo hat geschrieben:D.h. Generator-Objekte kommen ausschliesslich von Funktionen, die ein yield enthalten, korrekt?
Oder gibts doch noch weitere Möglichkeiten, die zu erzeugen?
Generator-Ausdrücke geben auch Generatoren zurück ;-)
Das Leben ist wie ein Tennisball.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

EyDu hat es gesagt.

Was neuerdings auch geht, ist das hier:

Code: Alles auswählen

def nullgen():
    yield from []
Ich würde aber wohl sowas machen:

Code: Alles auswählen

>>> EMPTY_GEN = iter(())
>>> def foo():
...     return EMPTY_GEN
...
>>> next(foo())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
In specifications, Murphy's Law supersedes Ohm's.
BlackJack

@akis.kapo: Und was ist jetzt der Unterschied zwischen einem Generator und einem `listiterator` der Dich stört? Das sind doch nur zwei Namen für die selbe Schnittstelle. Solange `next()` bis es zu `StopIteration` kommt. Was hätte denn das Ergebnis von `generator()` für andere Eigenschaften als das von `iter()`!?
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

BlackJack hat geschrieben:@akis.kapo: Und was ist jetzt der Unterschied zwischen einem Generator und einem `listiterator` der Dich stört? Das sind doch nur zwei Namen für die selbe Schnittstelle. Solange `next()` bis es zu `StopIteration` kommt. Was hätte denn das Ergebnis von `generator()` für andere Eigenschaften als das von `iter()`!?
Wie schon gesagt, es macht praktisch keinen Unterschied - daher sehe ich es grundsätzlich schon ein.

Aber,
trotzdem finde ich, war es eine berechtigte Frage, wie man ein leeres Generator-Objekt erstellt.

Evtl. kann man es der Einheitlichkeit wegen so umsetzen, für den (zugegeben exotischen) Fall,
dass jemand 'ne Typen-Überprüfung auf die Rückgabewerte hat.
(wieso auch immer das in einer duck-typed Sprache Sinn macht, sei mal dahingestellt,
schließlich sind die selben Funktionen unterstützt, obj.next(), iter(obj),...)

EyDu hat's eigentlich bereits erwähnt, aber ich wusste anfangs nicht, was genau er meinte,
weil er hat kein konkretes Beispiel genannt. Durch rum-experimentieren bin ich dann unabhängig
davon auf eine Generator-Expression gestoßen, die man hier gut verwenden kann.

Generator-Ausdruck am Beispiel von iter() ohne Funktionsdeklaration:

Code: Alles auswählen

>>> from myrange import *
>>> myrange(8)
[0, 1, 2, 3, 4, 5, 6, 7]
>>> myrange_gen(8)
<generator object inner_myrange_gen at 0x6ffffedf9b0>
>>> iter(())
<tupleiterator object at 0x6ffffee65d0>
>>> iter([])
<listiterator object at 0x6ffffee6610>
>>> iter(_ for _ in [])
<generator object <genexpr> at 0x6ffffedf9b0>
Mit anderen Worten, einfach aus iter([]) oder iter(()) ein iter(_ for _ in []) machen,
das wäre dann eine sog. Generator-Expression, welche ein "generator object" spuckt.

Und wie bereits erwähnt, es ist mir eigentlich egal was zurückgegeben wird, aber Hauptsache es kommt immer dasselbe zurück.
(oder noch besser gesagt: es ist mir auch egal, ob immer derselbe Typ zurückkommt, Hauptsache ich wüsste, wie ich es anstelle, falls es doch wichtig wäre.)

Fällt jemanden evtl. ein noch kürzerer Eindruck ein? Kürzeste Generator Expression?
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

UPDATE / SOLVED:

Der finale Code müste dann so aussehen:

Code: Alles auswählen

def myrange_gen(stop,start=None,step=None):
    if start is None:
        start = 0
    else:
        # [start,] stop [, step]
        start, stop = stop, start  
   
    if step is None:
        step = 1
    if start is stop:
        return iter(_ for _ in [])
    if step == 0:
        return iter(_ for _ in [])
    if start < stop and step < 0:
        return iter(_ for _ in [])
    if start > stop and step > 0:
        return iter(_ for _ in [])
 
    def inner_myrange_gen(stop,start,step):
        if start < stop:
            while start < stop:
                yield start
                start += step
        else:
            while start > stop:
                yield start
                start += step
 
    return inner_myrange_gen(stop,start,step)
 
def myrange(stop,start=None,step=None):
    return list(myrange_gen(stop,start,step))
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@akis.kapo: Zum eigentlichen Code: außer in Ausnahmefällen wird per is nur mit None verglichen, Zeile 10 funktioniert bei CPython zufällig für kleine Zahlen, bei größeren aber schon nicht mehr. Die Vergleiche in Zeile 10, 14 und 16 sind sowieso überflüssig, da die while-Schleife für diese Fälle 0 mal durchlaufen wird. Wenn Du den Defaultwert von step auf 1 setzt sind Zeile 8 und 9 auch unnötig.
Warum machst Du überhaupt eine innere Funktion. Hättest Du yield auf der obersten Ebene, wäre das Leerer-Generator-Problem erst gar nicht aufgetreten:

Code: Alles auswählen

import operator

def myrange_gen(stop, start=None, step=1):
    if start is None:
        start = 0
    else:
        # [start,] stop [, step]
        start, stop = stop, start  
   
    if step == 0:
        return
    vergleich = operator.gt if step < 0 else operator.lt
    while vergleich(start, stop):
        yield start
        start += step
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@akis.kapo: Eine Funktion/Methode ist schon ein Generator, sobald ein ``yield`` darin vorkommt. Wenn der Generator vorzeitig aussteigen soll, dann geht das mit einem "nackten" ``return`` (in Schleifen auch via ``break``). Das ist deutlich einfacher, finde ich.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Schon klar, aber es wird noch zw. Generator-Funktionen und Generator-Ausdrücken unterschieden,
letztere sind kürzer und daher für so Sonderspezial-Fälle (leeren Generator zurückgeben) noch einfacherer.

Siehe:

Code: Alles auswählen

def foo():
    yield
# VS
iter(_ for _ in [])
EDIT: hier ein sinnvolles Beispiel zur Veranschaulichung des Unterschieds zw. Gen-Funkt. und Gen-Ausdr.:

Code: Alles auswählen

# generator function
def foo_sum():
    for x in range(8):
        yield x*x
# generator expression
sum(x*x for x in range(8))
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Vergleiche ich deinen Code mit dem von Sirius3, dann sehe ich bei letzterem aber weitaus weniger Quelltext. :o
BlackJack

@akis.kapo: Auf einem Generatorausdruck `iter()` aufzurufen ist wenig sinnvoll:

Code: Alles auswählen

In [1]: a = iter(_ for _ in [])

In [2]: a
Out[2]: <generator object <genexpr> at 0xb70855cc>

In [3]: a = (_ for _ in [])

In [4]: a
Out[4]: <generator object <genexpr> at 0xb70bd8c4>
Ein Generatorausdruck heisst so weil er zu einem Generator-Objekt ausgewertet wird. Ein `iter()` ändert da nix mehr dran. ;-)

Wobei das Generatorausdrücke und Generatorfunktionen Objekte vom gleichen Typ zurückgeben müssen IMHO auch nicht als gegeben angesehen werden muss. Das ist in CPython (momentan) so, kann aber auch anders gelöst werden.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

snafu hat geschrieben:Vergleiche ich deinen Code mit dem von Sirius3, dann sehe ich bei letzterem aber weitaus weniger Quelltext. :o
Das ist richtig. Man muss schon den Kontext verstehen, sonst machen keine Posting hier einen Sinn.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Sirius3 hat geschrieben:... Zeile 10 funktioniert bei CPython zufällig für kleine Zahlen, bei größeren aber schon nicht mehr.
Du meinst weil ab einer bestimmten Größe von "normalen" Ints nach PythonInts (die mit L hinten) gearbeitet wird.
Daran habe ich garnicht gedacht. Das muss ich mir allgemein merken als Best Practice... :oops:
Sirius3 hat geschrieben:Die Vergleiche in Zeile 10, 14 und 16 sind sowieso überflüssig, da die while-Schleife für diese Fälle 0 mal durchlaufen wird. Wenn Du den Defaultwert von step auf 1 setzt sind Zeile 8 und 9 auch unnötig.
Kannst du dazu vielleicht nochmal was sagen damit ich's besser verstehe? Mich wundert es ein bischen.
Sirius3 hat geschrieben:Warum machst Du überhaupt eine innere Funktion. Hättest Du yield auf der obersten Ebene, wäre das Leerer-Generator-Problem erst gar nicht aufgetreten
Richtig. Weil ich zuerst die obere Codehälfte geschrieben habe und mir dann "mittendrin" die Idee mit der Trennung der Funktionen kam (eine Generator-Funktion und eine die immer fertige Listen zurückgibt).

Und ja, dein Code ist deutlich besser. Ich hab mich schon gewundert, ob das sein kann, dass das range() so kompliziert ist und so viele versch. Fallunterscheidungen braucht, kann ja nicht sein...
...nur leider wars nicht so klar.

EDIT:
@BJ
Oh mann, hinterher denk ich immer, hättest dir das auch selber denken können. Das mit (...) statt iter(...).
Also bisher scheint wohl (_ for _ in []) die kürzeste(, leere) Generator-Expression zu sein, wenn das niemand nochmals unterbietet.

Ich muss mir echt ein eigenes Python Cheat-Sheet bauen, um all diese ganzen kleinen "basics" immer auf einen Blick zu haben. :!:
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

akis.kapo hat geschrieben:Du meinst weil ab einer bestimmten Größe von "normalen" Ints nach PythonInts (die mit L hinten) gearbeitet wird.
Daran habe ich garnicht gedacht. Das muss ich mir allgemein merken als Best Practice... :oops:
Nee, das liegt an dem "is". is prüft auf Objektidentität und nicht auf Gleichheit. Da CPython kleine Zahlen bereits im Cache vorliegen hat und diese nicht immer neu erzeugt, funktioniert is nur zufällig für Zahlen von 0 bis 256. Danach liefert es False zurück. Wenn du auf Gleichheit testen willst, dann benutze ==.

Code: Alles auswählen

>>> for i, j in zip(range(250, 260), range(250, 260)):
...     print i, i is j
... 
250 True
251 True
252 True
253 True
254 True
255 True
256 True
257 False
258 False
259 False
Die genaue Anzahl der Zahlen im Cache ist natürlich fix und kann sich daher beliebig ändern.
Das Leben ist wie ein Tennisball.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Ja, ich merke gerade, es hätte auch ein einfacher Copy & Paste Fehler von Zeile 8 gewesen sein.
Einfach Zeile 8 kopiert, None durch stop ersetzt, "is" stehengelassen, statt durch "==" ersetzt, ... schon ist's geschehen.
Antworten