2011-12-22 17 views
9

Quando uso un generatore in un ciclo for, sembra "sapere", quando non ci sono più elementi restituiti. Ora, devo usare un generatore SENZA un ciclo for, e usare il prossimo() a mano, per ottenere l'elemento successivo. Il mio problema è, come faccio a sapere se non ci sono più elementi?Python 3.x: verifica se il generatore ha elementi rimanenti

I so only: successivo() genera un'eccezione (StopIteration), se non è rimasto nulla, MA non è un'eccezione un po 'troppo "pesante" per un problema così semplice? Non c'è un metodo come has_next() o così?

Le seguenti linee dovrebbero rendere chiaro, quello che voglio dire:

#!/usr/bin/python3 

# define a list of some objects 
bar = ['abc', 123, None, True, 456.789] 

# our primitive generator 
def foo(bar): 
    for b in bar: 
     yield b 

# iterate, using the generator above 
print('--- TEST A (for loop) ---') 
for baz in foo(bar): 
    print(baz) 
print() 

# assign a new iterator to a variable 
foobar = foo(bar) 

print('--- TEST B (try-except) ---') 
while True: 
    try: 
     print(foobar.__next__()) 
    except StopIteration: 
     break 
print() 

# assign a new iterator to a variable 
foobar = foo(bar) 

# display generator members 
print('--- GENERATOR MEMBERS ---') 
print(', '.join(dir(foobar))) 

L'uscita è la seguente:

--- TEST A (for loop) --- 
abc 
123 
None 
True 
456.789 

--- TEST B (try-except) --- 
abc 
123 
None 
True 
456.789 

--- GENERATOR MEMBERS --- 
__class__, __delattr__, __doc__, __eq__, __format__, __ge__, __getattribute__, __gt__, __hash__, __init__, __iter__, __le__, __lt__, __name__, __ne__, __new__, __next__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __str__, __subclasshook__, close, gi_code, gi_frame, gi_running, send, throw 

Grazie a tutti, e buona giornata! :)

risposta

5

Le due affermazioni che hai scritto riguardano la ricerca della fine del generatore esattamente nello stesso modo. Il ciclo for semplicemente chiama .next() finché l'eccezione StopIteration non viene sollevata e quindi termina.

http://docs.python.org/tutorial/classes.html#iterators

In quanto tale non credo che in attesa per l'eccezione StopIteration è un modo 'pesante' per affrontare il problema, è il modo in cui i generatori sono stati progettati per essere utilizzati.

16

Questa è una grande domanda. Proverò a mostrarti come possiamo usare le abilità introspettive di Python e l'open source per ottenere una risposta. Possiamo usare il modulo dis per sbirciare dietro le quinte e vedere come l'interprete CPython implementa un ciclo for su un iteratore.

>>> def for_loop(iterable): 
...  for item in iterable: 
...   pass # do nothing 
...  
>>> import dis 
>>> dis.dis(for_loop) 
    2   0 SETUP_LOOP    14 (to 17) 
       3 LOAD_FAST    0 (iterable) 
       6 GET_ITER    
     >> 7 FOR_ITER     6 (to 16) 
      10 STORE_FAST    1 (item) 

    3   13 JUMP_ABSOLUTE   7 
     >> 16 POP_BLOCK    
     >> 17 LOAD_CONST    0 (None) 
      20 RETURN_VALUE   

Il bit succoso sembra essere l'opcode FOR_ITER. Non possiamo immergerci più a fondo con dis, quindi cerchiamo FOR_ITER nel codice sorgente dell'interprete CPython. Se fai un giro, lo trovi in ​​Python/ceval.c; è possibile visualizzarlo here. Ecco il tutto:

TARGET(FOR_ITER) 
     /* before: [iter]; after: [iter, iter()] *or* [] */ 
     v = TOP(); 
     x = (*v->ob_type->tp_iternext)(v); 
     if (x != NULL) { 
      PUSH(x); 
      PREDICT(STORE_FAST); 
      PREDICT(UNPACK_SEQUENCE); 
      DISPATCH(); 
     } 
     if (PyErr_Occurred()) { 
      if (!PyErr_ExceptionMatches(
          PyExc_StopIteration)) 
       break; 
      PyErr_Clear(); 
     } 
     /* iterator ended normally */ 
     x = v = POP(); 
     Py_DECREF(v); 
     JUMPBY(oparg); 
     DISPATCH(); 

Vedi come funziona? Cerchiamo di afferrare un oggetto dall'iteratore; se falliamo, controlliamo quale eccezione è stata sollevata. Se è StopIteration, lo cancelliamo e consideriamo esaurito l'iteratore.

Quindi, come fa un ciclo for "basta sapere" quando un iteratore è stato esaurito? Risposta: non lo fa - deve cercare di afferrare un elemento. Ma perché?

Parte della risposta è la semplicità. Parte della bellezza di implementare gli iteratori è che devi definire solo un'operazione: prendi il prossimo elemento. Ma ancora più importante, rende gli iteratori pigri: produrranno solo i valori che devono assolutamente.

Infine, se ti manca davvero questa funzione, è banale implementarla tu stesso. Ecco un esempio:

class LookaheadIterator: 

    def __init__(self, iterable): 
     self.iterator = iter(iterable) 
     self.buffer = [] 

    def __iter__(self): 
     return self 

    def __next__(self): 
     if self.buffer: 
      return self.buffer.pop() 
     else: 
      return next(self.iterator) 

    def has_next(self): 
     if self.buffer: 
      return True 

     try: 
      self.buffer = [next(self.iterator)] 
     except StopIteration: 
      return False 
     else: 
      return True 


x = LookaheadIterator(range(2)) 

print(x.has_next()) 
print(next(x)) 
print(x.has_next()) 
print(next(x)) 
print(x.has_next()) 
print(next(x)) 
+1

Ho appena realizzato che voglio fare 'dis' per studiare' numpy'. ;) – n611x007

0

Non è possibile sapere in anticipo sulla fine del ciclo di iteratore nel caso generale, a causa di codice arbitrario potrebbe essere necessario eseguire per decidere sulla fine.Gli elementi tampone potrebbero aiutare a rivelare le cose a costi - ma questo è raramente utile.

In pratica la domanda sorge spontanea quando si vuole prendere solo uno o pochi elementi da un iteratore per ora, ma non si vuole scrivere quel brutto codice di gestione delle eccezioni (come indicato nella domanda). In effetti è non-pitonico mettere il concetto "StopIteration" nel normale codice dell'applicazione. E la gestione delle eccezioni a livello di Python è piuttosto dispendiosa in termini di tempo, in particolare quando si tratta solo di prendere un elemento.

Il modo divinatorio per gestire tali situazioni miglior utilizza o for .. break [.. else] come:

for x in iterator: 
    do_something(x) 
    break 
else: 
    it_was_exhausted() 

o usando la funzione built next() con difetto come

x = next(iterator, default_value) 

o utilizzando aiutanti iteratore esempio da itertools modulo per cose come RICONFIGURAZIONE:

max_3_elements = list(itertools.islice(iterator, 3)) 

Alcuni iteratori comunque espongono un "lunghezza suggerimento" (PEP424):

>>> gen = iter(range(3)) 
>>> gen.__length_hint__() 
3 
>>> next(gen) 
0 
>>> gen.__length_hint__() 
2 

Nota: iterator.__next__() non deve essere utilizzato per il normale codice app. Ecco perché lo hanno rinominato da iterator.next() in Python2. E usare next() senza default non è molto meglio ...

Problemi correlati