2016-05-17 20 views
8

Da quello che ho capito, un ciclo for x in a_generator: foo(x) in Python è grosso modo equivalente a questo:È in loop un generatore in loop su quello stesso generatore sicuro in Python?

try: 
    while True: 
     foo(next(a_generator)) 
except StopIteration: 
    pass 

Questo suggerisce che qualcosa di simile:

for outer_item in a_generator: 
    if should_inner_loop(outer_item): 
     for inner_item in a_generator: 
      foo(inner_item) 
      if stop_inner_loop(inner_item): break 
    else: 
     bar(outer_item) 

sarebbe fare due cose:

  1. Non sollevare eccezioni, segfault o qualcosa del genere
  2. Iterate over y fino a raggiungere lo x dove should_inner_loop(x) restituisce la verità, quindi eseguire il ciclo su di esso nel numero interno for fino a stop_inner_loop(thing) restituisce true. Quindi, il ciclo esterno riprende dove quello interno è stato interrotto.

Dai miei test dichiaratamente non molto buoni, sembra funzionare come sopra. Tuttavia, non sono riuscito a trovare nulla nelle specifiche a garanzia che questo comportamento sia costante tra gli interpreti. C'è qualcosa che dice o implica che posso essere sicuro che sarà sempre così? Può causare errori o funzionare in qualche altro modo? (Vale a dire fare qualcosa di diverso da ciò che è descritto sopra


NB Il codice equivalente di cui sopra è tratto dalla mia esperienza personale, non so se in realtà è preciso È per questo che sto chiedendo

+3

Anche se la tua logica è valida per quanto riguarda gli elementi che vengono consumati e quando, sarà difficile per qualcun altro (incluso il tuo sé futuro) ricostruire insieme cosa sta succedendo. –

+0

@ JaredGoguen Sono d'accordo. Nel mio codice attuale, ho il ciclo interno estratto in un altro metodo, ma ho pensato che l'intento della domanda sarebbe stato più chiaro se non avessi un altro metodo. –

risposta

6

TL; DR: è al sicuro con CPython (ma non ho trovato alcuna specificazione di questo), anche se non può fare quello che vuoi fare.


In primo luogo, parliamo della tua prima ipotesi, l'equivalenza.

Un ciclo for effettivamente chiama prima iter() sull'oggetto, quindi esegue next() sul risultato, finché non ottiene StopIteration.

Ecco il codice byte rilevante (una forma basso livello di Python, utilizzato dall'interprete stesso):

>>> import dis 
>>> def f(): 
... for x in y: 
... print(x) 
... 
>>> dis.dis(f) 
    2   0 SETUP_LOOP    24 (to 27) 
       3 LOAD_GLOBAL    0 (y) 
       6 GET_ITER 
     >> 7 FOR_ITER    16 (to 26) 
      10 STORE_FAST    0 (x) 

    3   13 LOAD_GLOBAL    1 (print) 
      16 LOAD_FAST    0 (x) 
      19 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      22 POP_TOP 
      23 JUMP_ABSOLUTE   7 
     >> 26 POP_BLOCK 
     >> 27 LOAD_CONST    0 (None) 
      30 RETURN_VALUE 

GET_ITER chiamate iter(y) (che si definisce y.__iter__()) e spinge il suo risultato in pila (pensare come un gruppo di variabili locali senza nome), quindi entra nel ciclo a FOR_ITER, che chiama (che di per sé chiama <iterator>.__next__()), quindi esegue il codice all'interno del ciclo e JUMP_ABSOLUTE rende l'esecuzione torna a FOR_ITER.


Ora, per la sicurezza:

Questi sono i metodi di un generatore: https://hg.python.org/cpython/file/101404/Objects/genobject.c#l589 Come si può vedere in line 617, l'attuazione di __iter__() è PyObject_SelfIter, la cui attuazione è possibile trovare here. PyObject_SelfIter restituisce semplicemente l'oggetto stesso (ad esempio il generatore).

Quindi, quando si nidificano i due anelli, entrambi iterano sullo stesso iteratore. E, come hai detto, stanno solo chiamando next() su di esso, quindi è sicuro.

Ma attenzione: il circuito interno consumerà oggetti che non verranno consumati dal circuito esterno. Anche se questo è quello che vuoi fare, potrebbe non essere molto leggibile.

Se questo non è quello che si vuole fare, si consideri itertools.tee(), che bufferizza l'output di un iteratore, consentendo di eseguire iterazioni sul suo output due volte (o più). Questo è efficace solo se gli iteratori a tee si mantengono vicini l'uno all'altro nel flusso di output; se un tee iterator sarà completamente esaurito prima che l'altro venga utilizzato, è meglio chiamare lo list sull'iteratore per materializzare un elenco al di fuori di esso.

+0

Ehi, penso che poiché ho formulato la mia domanda male, ho ottenuto alcune risposte alla domanda sbagliata. Potresti farmi un favore e ricontrollare questa risposta con la domanda leggermente aggiornata? –

+0

Il punto 1 era quello che ho capito come sicurezza, quindi sì, è sicuro. Il punto 2 descrive ciò che effettivamente fa (che ho trattato nei due ultimi paragrafi) –

+0

Impressionante! Non ho capito bene l'ultimo pezzetto di 'itertools.tee()', ma sembrava che stesse affrontando la questione con l'equivoco che avevo lasciato a tutti. –

3

No.. , non è al sicuro (come in, non otterremo il risultato che ci si sarebbe aspettato)

Considerate questo:..

a = (_ for _ in range(20)) 
for num in a: 
    print(num) 

Naturalmente, avremo 0-19 stampato

Ora aggiungiamo un po 'di codice:

a = (_ for _ in range(20)) 
for num in a: 
    for another_num in a: 
     pass 
    print(num) 

L'unica cosa che sarà stampato è 0. Nel momento in cui arriviamo alla seconda iterazione del ciclo esterno, il generatore sarà già esaurito dal circuito interno.

Possiamo anche fare questo:

a = (_ for _ in range(20)) 
for num in a: 
    for another_num in a: 
     print(another_num) 

se era sicuro che ci si aspetterebbe di ottenere 0 a 19 stampati 20 volte, ma in realtà farlo stampati solo una volta, per lo stesso motivo che ho citato sopra.

+4

Bene, dipende dal risultato atteso. Se includi l'idea di 'break' che l'OP ha usato, e se capisci che continuerà a scorrere il generatore * da dove era *, allora direi che ottieni un comportamento previsto, per i generatori. – dwanderson

+0

Ho chiarito cosa intendo: questo comportamento è quello che sto cercando. Grazie mille! –

2

Non è davvero una risposta alla tua domanda, ma ti consiglio di non farlo perché il codice non è leggibile. Mi ci è voluto un po 'per capire che stavi usando lo due volte anche se questo è l'intero punto della tua domanda. Non far confondere un futuro lettore con questo. Quando vedo un ciclo annidato, non mi aspetto quello che hai fatto e il mio cervello ha difficoltà a vederlo.

farei così:

def generator_with_state(y): 
    state = 0 
    for x in y: 
     if isinstance(x, special_thing): 
      state = 1 
      continue 
     elif state == 1 and isinstance(x, signal): 
      state = 0 
     yield x, state 

for x, state in generator_with_state(y): 
    if state == 1: 
     foo(x) 
    else: 
     bar(x) 
+0

Ecco come lo sto facendo nel mio codice reale. Ho chiesto con un po 'più confuso perché pensavo che l'intento sarebbe stato più chiaro - looping sul generatore all'interno di un altro ciclo. Scuse. –

+0

Non c'è bisogno di scusarsi. Non è sempre ovvio che cosa renderà il codice più o meno leggibile. Ma penso che il fatto che tutti sembrino confusi riguardo alla tua domanda dimostra il mio punto. –

+0

Rileggi il tuo codice e in realtà ... non è così simile a quello che sto facendo. Potrei solo dare un'occhiata al cellulare, ma è simile a _looked_; il mio codice attuale è che sto definendo una funzione che fa il ciclo, quindi se accade la cosa speciale, chiamiamo quella funzione invece di avere un ciclo interno. Sfortunatamente, questo schema non funzionerebbe per me; a parte questo, sembra buono! –