Creazione di un Iterator Python di base


427

Come si creerebbe una funzione iterativa (o un oggetto iteratore) in python?

+3

Ci sono due domande qui, entrambe importanti. Come rendere una classe iterabile (cioè che è possibile ripetere il ciclo con)? E come creare una funzione che restituisce una sequenza con valutazione lazy? 12 lug. 152015-07-12 13:26:31

+4

Un buon esercizio penso sia quello di scrivere una classe che rappresenti i numeri pari (una sequenza infinita). 12 lug. 152015-07-12 13:30:02

+1

@ColonelPanic: Ok, ho aggiunto l'esempio di numero infinito a [la mia risposta] (http://stackoverflow.com/a/7542261/208880). 04 feb. 162016-02-04 17:25:03

499

oggetti iteratori in pitone sono conformi al protocollo iteratore, che in pratica significa che forniscono due metodi:. __iter__() e next(). Lo __iter__ restituisce l'oggetto iteratore e viene richiamato implicitamente all'inizio dei cicli. Il metodo next() restituisce il valore successivo e viene chiamato implicitamente ad ogni incremento del ciclo. next() solleva un'eccezione StopIteration quando non ci sono più valori da restituire, che viene catturato in modo implicito dal ciclo dei costrutti per interrompere l'iterazione.

Ecco un semplice esempio di un contatore:

class Counter: 
    def __init__(self, low, high): 
     self.current = low 
     self.high = high 

    def __iter__(self): 
     return self 

    def next(self): # Python 3: def __next__(self) 
     if self.current > self.high: 
      raise StopIteration 
     else: 
      self.current += 1 
      return self.current - 1 


for c in Counter(3, 8): 
    print c 

questo stampa:

3 
4 
5 
6 
7 
8 

Questo è più facile da scrivere utilizzando un generatore, come coperto in una risposta precedente:

def counter(low, high): 
    current = low 
    while current <= high: 
     yield current 
     current += 1 

for c in counter(3, 8): 
    print c 

L'output stampato sarà lo stesso. Sotto il cofano, l'oggetto generatore supporta il protocollo iteratore e fa qualcosa di simile al contatore di classe.

L'articolo di David Mertz, Iterators and Simple Generators, è una buona introduzione.

+45

Si noti che la funzione 'next()' non restituisce valori 'yield', li restituisce '. 17 ott. 122012-10-17 07:03:44

+46

Questo non è valido in Python 3 --- deve essere '__next __()'. 23 set. 132013-09-23 03:42:21

+2

Questa è principalmente una buona risposta, ma il fatto che ritorni da solo è un po 'non ottimale. Ad esempio, se hai usato lo stesso oggetto contatore in un ciclo double annidato probabilmente non otterrai il comportamento che intendevi. 06 feb. 142014-02-06 23:33:31

+12

No, gli iteratori DOVREBBE ritornare. Iterables restituisce gli iteratori, ma i file iterabili non devono implementare '__next__'. 'counter' è un iteratore, ma non è una sequenza. Non memorizza i suoi valori. Ad esempio, non dovresti utilizzare il contatore in un ciclo for doppio annidato. 21 feb. 142014-02-21 08:42:44

+3

Nell'esempio Counter, self.current deve essere assegnato in '__iter__' (oltre a' __init__'). Altrimenti, l'oggetto può essere iterato una sola volta. ad esempio se si dice 'ctr = Contatori (3, 8)', allora non è possibile utilizzare 'per c in ctr' più di una volta. 05 apr. 162016-04-05 23:00:00

  0

non dovrebbe il \ _ \ _ iter \ _ \ _ codice impostare il valore di self.current? 13 mar. 172017-03-13 02:01:54

  0

@ Curt: Assolutamente no. 'Counter' è un iteratore e gli iteratori dovrebbero essere ripetuti una volta sola. Se si ripristina 'self.current' in' __iter__', poi un ciclo nidificato sul 'Counter' sarebbe completamente rotto, e ogni sorta di comportamenti assunti di iteratori (che chiamare' iter' su di essi è idempotente) sono violati. Se vuoi essere in grado di iterare 'ctr' più di una volta, deve essere un iteratore non iteratore, in cui restituisce un nuovo iteratore ogni volta che viene richiamato' __iter__'. Cercando di combinare e abbinare (un iteratore che viene resettato implicitamente quando viene richiamato '__iter__') viola i protocolli. 24 feb. 182018-02-24 01:16:43

  0

Per esempio, se 'Counter' è stato quello di essere un non-iteratore iterabile, devi rimuovere la definizione di' __next__'/'next' del tutto, e probabilmente ridefinire' __iter__' in funzione del generatore della stessa forma come il generatore descritto alla fine di questa risposta (eccetto i limiti provenienti dagli argomenti di '__iter__', sarebbero argomenti per' __init__' salvati su 'self' e accessibili da' self' in '__iter__'). 24 feb. 182018-02-24 01:19:21

  0

BTW, una cosa utile da fare se si vuole scrivere classi iteratore portatili è quello di definire uno 'next' o' __next__', quindi assegnare un nome all'altro ('prossima = __next__' o' __next__ = next' a seconda della nome hai dato il metodo). Avere entrambi i nomi definiti significa che funziona su entrambi Py2 e Py3 senza modifiche al codice sorgente. 24 feb. 182018-02-24 01:43:52


97

Prima di tutto la itertools module è incredibilmente utile per tutti i tipi di casi in cui un iteratore sarebbe utile, ma qui è tutto ciò che serve per creare un iteratore in pitone:

resa

Non è bello? Il rendimento può essere utilizzato per sostituire un normale restituire in una funzione. Restituisce l'oggetto allo stesso modo, ma invece di distruggere lo stato e uscire, salva lo stato per quando si desidera eseguire l'iterazione successiva. Ecco un esempio in azione tirato direttamente dalla itertools function list:

def count(n=0): 
    while True: 
     yield n 
     n += 1 

Come indicato nella descrizione funzioni (è il conteggio () funzione dal modulo itertools ...), produce un iteratore che restituisce interi consecutivi iniziando con n.

Generator expressions sono un intero altro tipo di worm (worm fantastici!). Possono essere usati al posto di un List Comprehension per risparmiare memoria (le list comprehensions creano un elenco in memoria che viene distrutto dopo l'uso se non assegnato a una variabile, ma le espressioni del generatore possono creare un oggetto generatore ... che è un modo elegante per dire Iterator). Ecco un esempio di definizione generatore di espressione:

gen = (n for n in xrange(0,11)) 

Questo è molto simile alla nostra definizione iteratore sopra tranne la gamma viene predeterminato per essere tra 0 e 10.

Ho scoperto xrange() (sorpreso non l'avevo visto prima ...) e l'ho aggiunto all'esempio sopra. xrange() è una versione iterabile del range () che presenta il vantaggio di non pre-compilare l'elenco. Sarebbe molto utile se si ha un corpus gigantesco di dati da iterare e aveva solo la memoria così tanto per farlo in

+17

come di Python 3.0 non c'è un xrange() e la nuova gamma() si comporta come il vecchio xrange() 18 dic. 082008-12-18 17:30:08

+6

Si dovrebbe comunque utilizzare xrange in 2._, perché 2to3 traduce automaticamente. 22 lug. 112011-07-22 18:03:40


305

Ci sono quattro modi per costruire una funzione iterativa:

Esempi:

# generator 
def uc_gen(text): 
    for char in text: 
     yield char.upper() 

# generator expression 
def uc_genexp(text): 
    return (char.upper() for char in text) 

# iterator protocol 
class uc_iter(): 
    def __init__(self, text): 
     self.text = text 
     self.index = 0 
    def __iter__(self): 
     return self 
    def __next__(self): 
     try: 
      result = self.text[self.index].upper() 
     except IndexError: 
      raise StopIteration 
     self.index += 1 
     return result 

# getitem method 
class uc_getitem(): 
    def __init__(self, text): 
     self.text = text 
    def __getitem__(self, index): 
     result = self.text[index].upper() 
     return result 

per vedere tutti i quattro metodi in azione:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem: 
    for ch in iterator('abcde'): 
     print ch, 
    print 

che si traduce in:

A B C D E 
A B C D E 
A B C D E 
A B C D E 

Nota:

I due generatore i tipi (uc_gen e uc_genexp) non possono essere reversed(); l'iteratore semplice (uc_iter) richiede il metodo magico __reversed__ (che deve restituire un nuovo iteratore che va indietro); e il GetItem iterare (uc_getitem) deve avere il metodo __len__ magia:

# for uc_iter 
    def __reversed__(self): 
     return reversed(self.text) 

    # for uc_getitem 
    def __len__(self) 
     return len(self.text) 

di rispondere alla domanda secondaria del colonnello panico su un infinito iteratore pigramente valutato, qui ci sono quegli esempi, utilizzando ciascuno dei quattro metodi di cui sopra:

# generator 
def even_gen(): 
    result = 0 
    while True: 
     yield result 
     result += 2 


# generator expression 
def even_genexp(): 
    return (num for num in even_gen()) # or even_iter or even_getitem 
             # not much value under these circumstances 

# iterator protocol 
class even_iter(): 
    def __init__(self): 
     self.value = 0 
    def __iter__(self): 
     return self 
    def __next__(self): 
     next_value = self.value 
     self.value += 2 
     return next_value 

# getitem method 
class even_getitem(): 
    def __getitem__(self, index): 
     return index * 2 

import random 
for iterator in even_gen, even_genexp, even_iter, even_getitem: 
    limit = random.randint(15, 30) 
    count = 0 
    for even in iterator(): 
     print even, 
     count += 1 
     if count >= limit: 
      break 
    print 

che si traduce in (almeno per la mia corsa del campione):

0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 
+4

Mi piace questo riepilogo perché è completo. Questi tre modi (rendimento, espressione del generatore e iteratore) sono essenzialmente gli stessi, sebbene alcuni siano più convenienti di altri. L'operatore yield cattura la "continuazione" che contiene lo stato (ad esempio l'indice che stiamo facendo). L'informazione viene salvata nella "chiusura" della continuazione. Il modo iteratore salva le stesse informazioni all'interno dei campi dell'iteratore, che è essenzialmente la stessa cosa di una chiusura. Il metodo __getitem__ è leggermente diverso perché indicizza nei contenuti e non è di natura iterativa. 05 lug. 132013-07-05 01:04:22

  0

Non stai incrementando l'indice nel tuo ultimo approccio, 'uc_getitem()'.In realtà sulla riflessione, non dovrebbe incrementare l'indice, perché non lo mantiene. Ma non è neanche un modo per astrarre l'iterazione. 05 nov. 132013-11-05 15:25:55

+2

@metaperl: In realtà, lo è. In tutti e quattro i casi sopra puoi usare lo stesso codice per iterare. 05 nov. 132013-11-05 16:37:21


79

Vedo che alcuni di voi stanno facendo return self in __iter__. Volevo solo sottolineare che __iter__ sé può essere un generatore (eliminando così la necessità di __next__ e alzando StopIteration eccezioni)

class range: 
    def __init__(self,a,b): 
    self.a = a 
    self.b = b 
    def __iter__(self): 
    i = self.a 
    while i < self.b: 
     yield i 
     i+=1 

Naturalmente qui si potrebbe anche fare direttamente un generatore, ma per le classi più complesse si può essere utile.

+5

Ottimo! È una scrittura così noiosa semplicemente "return self" in "__iter__". Quando stavo per provare a usare 'yield' in esso ho trovato che il tuo codice faceva esattamente quello che volevo provare. 05 feb. 132013-02-05 19:32:35

+2

Ma in questo caso, come si implementerebbe 'next()'? 'return iter (self) .next()'? 05 apr. 132013-04-05 19:52:12

+4

@Lenna, è già "implementato" perché iter (self) restituisce un iteratore, non un'istanza di intervallo. 07 apr. 132013-04-07 17:31:31

  0

@Manux 'iter (intervallo (5,10)). Next()' è un po 'macchinoso. Evidentemente un cattivo esempio di comportamento 'next'. Sono ancora interessato a come assegnare all'istanza di intervallo un attributo 'next'. 24 apr. 132013-04-24 19:06:36

+2

Questo è il modo più semplice per farlo e non implica il dover tenere traccia di ad es. '' self.current'' o qualsiasi altro contatore. Questa dovrebbe essere la risposta più votata! 31 mar. 142014-03-31 13:35:53

  0

La differenza: "__iter__" essendo un generatore è un oggetto diverso dall'istanza 'range()'. A volte questo è importante, a volte no. 09 nov. 142014-11-09 19:42:19

  0

Non dovresti usare 'iter (range (5,10)). Next()' comunque. Il modo corretto è 'next (iter (range (5,10)))'. Il 'next' builtin è lì esattamente così non devi preoccuparti se" self "viene restituito o meno in questa situazione. 14 mar. 162016-03-14 20:41:59

  0

up-votato - questo metodo funziona anche più come previsto (rispetto alla risposta accettata) per qualcosa come 'r = range (5); list_of_lists = list ([ri, list (r)] per ri in r) ' 17 set. 162016-09-17 16:09:23

  0

È interessante che' __iter__' non debba generare StopIteration. Un problema con la definizione solo di '__iter__' è che' next (myiterator) 'non funziona se' __next__' non 'restituisce' i singoli elementi. La necessità di usare 'next (iter (myiterator))' non è un sostituto saggio. 18 apr. 172017-04-18 14:42:52

  0

Per essere chiari, questo approccio rende la classe * iterabile *, ma non un * iteratore *. Otterrai nuovi iteratori * ogni volta che chiami 'iter' su istanze della classe, ma non sono loro stessi istanze della classe. 24 feb. 182018-02-24 01:25:07

  0

@MadPhysicist: Su Python 2, 'iter (intervallo (5,10)). Next()' e 'next (iter (intervallo (5,10)))' sono già esattamente equivalenti. Il vantaggio di 'next' come funzione non ha nulla a che fare se' self' viene restituito da '__iter__' (il comportamento è identico per entrambi i frammenti di codice). I vantaggi della funzione built-in 'next' sono: 1. Funziona allo stesso modo su Py2 e Py3, anche se il metodo cambia i nomi tra loro e 2. Se applicabile, può essere fornito un secondo argomento per tornare nell'evento che l'iteratore è già esaurito, piuttosto che generare 'StopIteration'. 24 feb. 182018-02-24 01:27:54


3

Questa è una funzione iterabile senza yield. Si fa uso della funzione iter e una chiusura che mantiene suo stato in un mutevole (list) in ambito di inclusione per pitone 2.

def count(low, high): 
    counter = [0] 
    def tmp(): 
     val = low + counter[0] 
     if val < high: 
      counter[0] += 1 
      return val 
     return None 
    return iter(tmp, None) 

Per Python 3, lo stato di chiusura è mantenuta in un immutabile nell'ambito racchiude e nonlocal viene utilizzato in ambito locale per aggiornare la variabile di stato.

def count(low, high): 
    counter = 0 
    def tmp(): 
     nonlocal counter 
     val = low + counter 
     if val < high: 
      counter += 1 
      return val 
     return None 
    return iter(tmp, None) 

Test;

for i in count(1,10): 
    print(i) 
1 
2 
3 
4 
5 
6 
7 
8 
9 
  0

Apprezzo sempre un uso intelligente di 'iter 'di due argomenti, ma per essere chiari: questo è più complesso e meno efficiente rispetto all'uso di una funzione di generatore basata su' yield'; Python ha un sacco di supporto per interprete per le funzioni di generatore basate su 'yield' di cui non puoi trarre vantaggio qui, rendendo questo codice notevolmente più lento. Votato comunque. 24 feb. 182018-02-24 01:30:05


7

Questa domanda riguarda gli oggetti iterabili, non gli iteratori. In Python, sequenze sono iterabili troppo quindi un modo per rendere una classe iterabile è a comportarsi come una sequenza, cioè dare __getitem__ e __len__ metodi. Ho provato questo su Python 2 e 3.

class CustomRange: 

    def __init__(self, low, high): 
     self.low = low 
     self.high = high 

    def __getitem__(self, item): 
     if item >= len(self): 
      raise IndexError("CustomRange index out of range") 
     return self.low + item 

    def __len__(self): 
     return self.high - self.low 


cr = CustomRange(0, 10) 
for i in cr: 
    print(i)