Ausbeute in Listenverständnissen und Generatorausdrücken

76

Das folgende Verhalten erscheint mir eher uninteressant (Python 3.4):

>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]

Die Zwischenwerte der letzten Zeile sind eigentlich nicht immer None, sie sind was auch immer wir sendin den Generator, äquivalent (ich denke) zu folgendem Generator:

def f():
   for i in range(3):
      yield (yield i)

Es kommt mir komisch vor, dass diese drei Zeilen überhaupt funktionieren. Die Referenz besagt, dass dies yieldnur in einer Funktionsdefinition zulässig ist (obwohl ich es möglicherweise falsch lese und / oder es einfach aus der älteren Version kopiert wurde). Die ersten beiden Zeilen erzeugen ein SyntaxErrorin Python 2.7, die dritte Zeile jedoch nicht.

Es scheint auch seltsam

  • dass ein Listenverständnis einen Generator und keine Liste zurückgibt
  • und dass der in eine Liste konvertierte Generatorausdruck und das entsprechende Listenverständnis unterschiedliche Werte enthalten.

Könnte jemand mehr Informationen geben?

Zabolekar
quelle

Antworten:

75

Hinweis : Dies war ein Fehler in der Behandlung von yieldVerständnis- und Generatorausdrücken durch CPython , der in Python 3.8 behoben wurde und in Python 3.7 eine Warnung vor Verfall enthielt. Weitere Informationen finden Sie im Python-Fehlerbericht und in den neuen Einträgen für Python 3.7 und Python 3.8 .

Generatorausdrücke sowie Mengen- und Diktatverständnisse werden zu (Generator-) Funktionsobjekten kompiliert. In Python 3 werden Listenverständnisse gleich behandelt. Sie alle sind im Wesentlichen ein neuer verschachtelter Bereich.

Sie können dies sehen, wenn Sie versuchen, einen Generatorausdruck zu zerlegen:

>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (3)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               3 (None)
             26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Das Obige zeigt, dass ein Generatorausdruck zu einem Codeobjekt kompiliert wird, das als Funktion geladen wird ( MAKE_FUNCTIONerstellt das Funktionsobjekt aus dem Codeobjekt). Mit der .co_consts[0]Referenz können wir das für den Ausdruck generierte Codeobjekt sehen, das YIELD_VALUEgenau wie eine Generatorfunktion verwendet wird.

Als solches yieldfunktioniert der Ausdruck in diesem Kontext, da der Compiler diese als Funktionen in Verkleidung betrachtet.

Dies ist ein Fehler; yieldhat keinen Platz in diesen Ausdrücken. Die Python- Grammatik vor Python 3.7 erlaubt dies (weshalb der Code kompilierbar ist), aber die yieldAusdrucksspezifikation zeigt, dass die Verwendung yieldhier eigentlich nicht funktionieren sollte:

Der Ertragsausdruck wird nur beim Definieren einer Generatorfunktion verwendet und kann daher nur im Hauptteil einer Funktionsdefinition verwendet werden.

Es wurde bestätigt, dass dies ein Fehler in Ausgabe 10544 ist . Die Lösung des Fehlers ist die Verwendung von yieldund yield fromwird ein SyntaxErrorin Python 3.8 auslösen ; In Python 3.7 wird ein a ausgelöst,DeprecationWarning um sicherzustellen, dass Code dieses Konstrukt nicht mehr verwendet. Die gleiche Warnung wird in Python 2.7.15 und höher angezeigt, wenn Sie den -3Befehlszeilenschalter verwenden, der Python 3-Kompatibilitätswarnungen aktiviert.

Die Warnung 3.7.0b1 sieht folgendermaßen aus. Wenn Sie Warnungen in Fehler umwandeln, erhalten Sie eine SyntaxErrorAusnahme, wie Sie es in 3.8 tun würden:

>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
  File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension

Die Unterschiede zwischen der Funktionsweise yieldeines Listenverständnisses und yieldeines Generatorausdrucks ergeben sich aus den Unterschieden bei der Implementierung dieser beiden Ausdrücke. In Python 3 verwendet ein Listenverständnis LIST_APPENDAufrufe, um der Liste, die erstellt wird, den Anfang des Stapels hinzuzufügen, während ein Generatorausdruck stattdessen diesen Wert liefert. Durch Hinzufügen wird (yield <expr>)nur ein weiterer YIELD_VALUEOpcode hinzugefügt:

>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                13 (to 22)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 YIELD_VALUE
             16 LIST_APPEND              2
             19 JUMP_ABSOLUTE            6
        >>   22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

Der YIELD_VALUEOpcode bei den Bytecode-Indizes 15 bzw. 12 ist extra, ein Kuckuck im Nest. Für den Listenverständnis-gedrehten Generator haben Sie also 1 Ausbeute, die jedes Mal die Spitze des Stapels erzeugt (wobei die Spitze des Stapels durch den yieldRückgabewert ersetzt wird), und für die Generatorausdrucksvariante ergeben Sie die Spitze des Stapels (die Ganzzahl) und dann wieder ergeben , aber jetzt enthält der Stapel den Rückgabewert von yieldund Sie erhalten Nonedas zweite Mal.

Für das Listenverständnis wird dann die beabsichtigte listObjektausgabe weiterhin zurückgegeben, Python 3 sieht dies jedoch als Generator an, sodass der Rückgabewert stattdessen als Attribut an die StopIterationAusnahme angehängt valuewird:

>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3))  # avoid exhausting the generator
[0, 1, 2]
>>> try:
...     next(listgen)
... except StopIteration as si:
...     print(si.value)
... 
[None, None, None]

Diese NoneObjekte sind die Rückgabewerte aus den yieldAusdrücken.

Und um dies noch einmal zu wiederholen; Das gleiche Problem gilt auch für das Wörterbuch- und Set-Verständnis in Python 2 und Python 3. In Python 2 werden die yieldRückgabewerte weiterhin zum beabsichtigten Wörterbuch oder zum festgelegten Objekt hinzugefügt, und der Rückgabewert wird zuletzt "ausgegeben", anstatt an die StopIterationAusnahme angehängt zu werden :

>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]
Martijn Pieters
quelle
Beachten Sie, dass gemäß der Sprachspezifikation das yield-atominnerhalb eines Ausdrucks (innerhalb einer Generatorfunktion) zulässig ist. Dies könnte noch problematischer sein, wenn das yield-atomirgendwie falsch implementiert wird.
Skyking
1
@skyking: das sage ich; Die Grammatik erlaubt es. Der Fehler, auf den ich mich beziehe, ist der Versuch, a yield als Teil eines Generatorausdrucks innerhalb einer Generatorfunktion zu verwenden , wobei erwartet wird, dass dies yieldfür die Generatorfunktion gilt, nicht für den verschachtelten Bereich des Generatorausdrucks.
Martijn Pieters
Beeindruckend. Sehr informativ. Wenn ich es richtig verstanden habe, ist Folgendes passiert: Eine Funktion, die beides enthält yieldund return, wie dokumentiert, eine Generatorfunktion werden sollte, deren returned-Wert in der StopIterationAusnahme landen sollte , und der Bytecode für ein Listenverständnis mit yieldInsider-Looks (obwohl dies der Fall war) nicht beabsichtigt) genau wie der Bytecode einer solchen Funktion.
Zabolekar
@ Zabolekar: so etwas; Die Schritte sind ungefähr so: Der Compiler stößt auf ein Listenverständnis, erstellt also ein Codeobjekt. Der Compiler stößt auf einen yieldAusdruck und markiert das aktuelle Codeobjekt als Generator. Voila, wir haben eine Generatorfunktion.
Martijn Pieters
1
@Chris_Rands Die 2.7-Änderungen sind vorhanden, wenn Sie die -3Kompatibilitätswarnungen verwenden.
Martijn Pieters