56

il seguente comportamento sembra piuttosto controintuitivo a me (Python 3.4):resa in list comprehension e generatore di espressioni

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

I valori intermedi del l'ultima linea sono in realtà non sempre None, sono tutto ciò che send nella generatore, equivalente (credo) al seguente generatore:

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

E mi sembra strano che quelle tre righe funzionano affatto. Loafferma che yield è consentito solo in una definizione di funzione (sebbene possa essere leggibile erroneamente e/o semplicemente essere stato copiato dalla versione precedente). Le prime due righe producono uno SyntaxError in Python 2.7, ma la terza riga no.

Inoltre, sembra strano

  • che una comprensione elenco restituisce un generatore e non una lista
  • e che l'espressione generatore convertita in un elenco e nell'elenco corrispondente comprensione contengono valori diversi.

Qualcuno potrebbe fornire ulteriori informazioni?

risposta

52

Le espressioni del generatore e le regole di set e dict sono compilate per oggetti funzione (generatore). In Python 3, la comprensione delle liste ha lo stesso trattamento; sono tutti, in sostanza, un nuovo ambito nidificato.

Potete vedere questo se si tenta di smontare un generatore di espressione:

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

Quanto sopra dimostra che un generatore di espressione viene compilata in un oggetto di codice, caricato come una funzione (MAKE_FUNCTION crea l'oggetto funzione dal oggetto codice). Il riferimento .co_consts[0] ci consente di vedere l'oggetto codice generato per l'espressione e utilizza YIELD_VALUE proprio come farebbe una funzione generatore.

Come tale, l'espressione yield funziona in tale contesto, in quanto il compilatore li vede come funzioni in incognito.

Questo è un bug; yield non ha posto in queste espressioni. Il pitone grammatica prima Python 3.7 consente (che è il motivo per cui il codice è compilabile), ma il yield expression specification dimostra che l'utilizzo yield qui non dovrebbero mai funzionare:

l'espressione yield viene usata solo quando si definisce un generatore e quindi può essere utilizzato solo nel corpo di una definizione di funzione.

È stato riscontrato un errore in issue 10544. La risoluzione del bug è che usando yield e yield from sarà raise a SyntaxError in Python 3.8; in Python 3.7 it raises a DeprecationWarning per assicurare che il codice si fermi utilizzando questo costrutto. Vedrai lo stesso avvertimento in Python 2.7.15 e versioni successive se usi lo -3 command line switch per attivare gli avvisi di compatibilità con Python 3.

Il 3.7.L'avviso 0b1 ha questo aspetto; trasformando gli avvisi in errori ti dà un'eccezione SyntaxError, come si farebbe 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 

le differenze tra come yield in una comprensione lista e yield in un'espressione generatore di operare staminali dalle differenze nel modo in cui queste due espressioni sono implementati . In Python 3 una list comprehension utilizza le chiamate LIST_APPEND per aggiungere la parte superiore dello stack all'elenco in fase di creazione, mentre un'espressione generatore produce invece quel valore. Aggiungendo (yield <expr>) appena aggiunge un altro YIELD_VALUE opcode a uno:

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

Il YIELD_VALUE codice operativo a indici bytecode 15 e 12 rispettivamente è aggiunto, un cuculo nel nido. Quindi per il generatore di list-comprehension-turned hai 1 yield producendo la cima dello stack ogni volta (sostituendo la parte superiore della pila con il valore di ritorno yield), e per la variante di espressione del generatore ottieni la cima della pila (il numero intero) e quindi restituire di nuovo, ma ora lo stack contiene il valore di ritorno dello yield e si ottiene None per la seconda volta.

Per l'elenco comprensione allora, il inteso list uscita oggetto viene ancora restituito, ma Python 3 la considera come generatore in modo che il valore restituito viene invece fissato alla StopIteration exception come attributo value:

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

Quelli Gli oggetti None sono i valori restituiti dalle espressioni yield.

E ripetermelo ancora; questo stesso problema si applica anche al dizionario e alla comprensione dei set in Python 2 e Python 3; in Python 2 i valori di ritorno yield vengono ancora aggiunti al dizionario previsto o oggetto impostare, e il valore di ritorno è 'ceduto' ultima invece attaccati l'eccezione StopIteration:

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

nota che secondo le specifiche del linguaggio del ' yield-atom' è permesso all'interno di un'espressione (all'interno di una funzione generatore). Questo potrebbe essere ancora più problematico se il 'yield-atom' è in qualche modo male implementato. – skyking

+1

@skyking: questo è quello che sto dicendo; la grammatica lo consente. L'errore a cui mi riferisco sta tentando di usare un 'yield' * come parte di un'espressione di generatore all'interno di una funzione generatore *, dove l'aspettativa è che il' yield' si applica alla funzione generatore, non all'ambito nested dell'espressione generatore. –

+0

Wow. Davvero molto informativo. Quindi, se ho capito bene, è successo quanto segue: una funzione che contiene sia 'yield' che' return' dovrebbe, come è documentato, diventare una funzione generatore il cui valore 'return'ed dovrebbe finire nell'eccezione' StopIteration', e bytecode per una comprensione delle liste con 'yield' all'interno di look (sebbene non fosse previsto) proprio come il bytecode di tale funzione. – zabolekar