56

das folgende Verhalten mir eher eingängig scheint (Python 3.4):Ausbeute in Listenkomprehensionen und Generator Ausdrücke

>>> [(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 wir send in die Generator, Äquivalent (ich denke) zum folgenden Generator:

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

Es scheint mir komisch, dass diese drei Linien überhaupt funktionieren. Die Reference sagt, dass yield ist nur in einer Funktionsdefinition erlaubt (obwohl ich es falsch lesen und/oder es kann einfach von der älteren Version kopiert wurden). Die ersten zwei Zeilen erzeugen eine SyntaxError in Python 2.7, aber die dritte Zeile nicht.

Auch scheint es seltsam

  • , dass eine Liste Verständnis einen Generator zurückgibt und keine Liste
  • und dass der Generator Ausdruck in eine Liste umgewandelt und die entsprechende Liste Verständnis enthalten unterschiedliche Werte.

Könnte jemand mehr Informationen zur Verfügung stellen?

Antwort

52

Generatorausdrücke, Set- und Dict-Comprehensions werden zu (Generator-) Funktionsobjekten kompiliert. In Python 3 erhalten List Comprehensions die gleiche Behandlung; Sie sind alle im Wesentlichen ein neuer verschachtelter Bereich.

Sie können das sehen, wenn Sie versuchen, einen Generator Ausdruck 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 

Die oben zeigt, dass ein Generator Ausdruck zu einem Codeobjekt kompiliert wird, geladen als Funktion (MAKE_FUNCTION erstellt die Funktion Objekt aus dem Code-Objekt). Die Referenz .co_consts[0] lässt uns das Code-Objekt sehen, das für den Ausdruck generiert wurde, und es verwendet YIELD_VALUE genau wie eine Generatorfunktion.

Als solche funktioniert der Ausdruck yield in diesem Kontext, da der Compiler diese Funktionen als verkappte Funktionen sieht.

Dies ist ein Fehler; yield hat in diesen Ausdrücken keinen Platz. Der Python Grammatik vor Python 3.7 erlaubt es (weshalb der Code übersetzbar ist), aber die yield expression specification zeigt, dass die Verwendung yield hier nicht wirklich funktionieren:

Die Ausbeute Ausdruck nur dann verwendet wird, wenn ein Generator definieren Funktion und kann daher nur im Rumpf einer Funktionsdefinition verwendet werden.

Dies wurde als Fehler in issue 10544 bestätigt. Die Lösung des Fehlers ist, dass yield und yield fromraise a SyntaxError in Python 3.8; in Python 3.7 it raises a DeprecationWarning, um sicherzustellen, dass der Code mit diesem Konstrukt aufhört. In Python 2.7.15 und höher wird dieselbe Warnung angezeigt, wenn Sie die -3 command line switch Python 3-Kompatibilitätswarnungen aktivieren.

Die 3.7.0b1 Warnung sieht so aus; Warnungen in Fehler gibt Ihnen eine SyntaxError Ausnahme drehen, wie würden Sie in 3.8:

>>> [(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 dem, wie yield in einer Liste Verständnis und yield in einem Generator Ausdruck arbeiten stammen aus den Unterschieden in, wie diese beiden Ausdrücke umgesetzt werden . In Python 3 verwendet ein Listenverständnis LIST_APPEND Aufrufe, um den Anfang des Stapels zu der Liste hinzuzufügen, die erstellt wird, während ein Generator-Ausdruck stattdessen diesen Wert liefert. Hinzufügen in (yield <expr>) fügt nur einen weiteren YIELD_VALUE Opcode entweder:

>>> 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_VALUE Opcode bei Bytecode-Indizes 15 bzw. 12 Extra ist ein Kuckuck im Nest. Also haben Sie für das List-Comprehension-turned-generator eine Ausbeute, die jedes Mal die Spitze des Stapels erzeugt (indem Sie die oberste des Stapels durch den yield Rückgabewert ersetzen), und für die Generator-Ausdrucksvariante geben Sie die Spitze des Stapels (die ganze Zahl) und ergeben dann wieder, aber jetzt enthält der Stapel den Rückgabewert des yield und Sie None, die zweite Zeit.

Für die Liste Verständnis dann wird die beabsichtigte list Objektausgabe noch zurück, aber Python 3 sieht dies als Generator, so ist der Rückgabewert statt zum StopIteration exception als value Attribut gebunden ist:

>>> 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] 

Diejenigen None Objekte sind die Rückgabewerte aus den yield Ausdrücken.

Und um das noch einmal zu wiederholen; Dasselbe gilt für das Wörterbuch und das Verstehen von Python 2 und Python 3; in Python 2 die yield Rückgabewerte werden den beabsichtigten Wörterbuch noch hinzugefügt oder Objekt gesetzt, und der Rückgabewert wird zuletzt statt an die StopIteration Ausnahme ‚liefert‘:

>>> 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])] 
+0

Note, das die Sprachspezifikation gemäß dem ' yield-atom' ist innerhalb eines Ausdrucks erlaubt (innerhalb einer Generatorfunktion). Dies könnte noch problematischer sein, wenn das "yield-atom" irgendwie falsch implementiert wird. – skyking

+1

@skyking: das ist was ich sage; Die Grammatik erlaubt es. Der Fehler, auf den ich mich beziehe, versucht, einen 'yield' * als Teil eines Generatorausdrucks innerhalb einer Generatorfunktion * zu verwenden, wobei erwartet wird, dass der' yield' auf die Generatorfunktion und nicht auf den verschachtelten Bereich des Generatorausdrucks zutrifft. –

+0

Wow. Sehr informativ. Also, wenn ich richtig verstanden habe, passierte folgendes: Eine Funktion, die sowohl 'yield' als auch 'return' enthält, sollte, wie dokumentiert, zu einer Generatorfunktion werden, deren 'return'ed Wert in der' StopIteration' Exception landen sollte Bytecode für ein Listenverständnis mit 'yield' innerhalb von Looks (obwohl es nicht beabsichtigt war) genau wie der Bytecode einer solchen Funktion. – zabolekar

Verwandte Themen