Costruisci un Iteratore Python di base


569

Come si potrebbe creare una funzione iterativa (o oggetto iteratore) in Python?

Risposte:


650

Gli oggetti Iterator in Python sono conformi al protocollo iteratore, il che significa sostanzialmente che forniscono due metodi: __iter__() e __next__().

  • Il __iter__restituisce l'oggetto iteratore ed è implicitamente chiamato all'inizio del loop.

  • Il __next__()metodo restituisce il valore successivo e viene chiamato implicitamente ad ogni incremento del ciclo. Questo metodo solleva un'eccezione StopIteration quando non ci sono più valori da restituire, che viene implicitamente catturato dai costrutti di loop per fermare l'iterazione.

Ecco un semplice esempio di contatore:

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

    def __iter__(self):
        return self

    def __next__(self): # Python 2: def next(self)
        self.current += 1
        if self.current < self.high:
            return self.current
        raise StopIteration


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

Questo stamperà:

3
4
5
6
7
8

È più facile scrivere usando un generatore, come spiegato in una risposta precedente:

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

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

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

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


4
Questa è principalmente una buona risposta, ma il fatto che restituisca sé è un po 'non ottimale. Ad esempio, se si utilizza lo stesso oggetto contatore in un ciclo doppiamente annidato per probabilmente non si otterrebbe il comportamento che intendevi.
Casey Rodarmor,

22
No, gli iteratori DOVREBBERO tornare da soli. Iterables restituiscono iteratori, ma iterables non dovrebbero implementarsi __next__. counterè un iteratore, ma non è una sequenza. Non memorizza i suoi valori. Ad esempio, non dovresti usare il contatore in un ciclo for doppio annidato.
leewz,

4
Nell'esempio Counter, self.current dovrebbe essere assegnato in __iter__(oltre a in __init__). Altrimenti, l'oggetto può essere ripetuto una sola volta. Ad esempio, se dici ctr = Counters(3, 8), non puoi usarlo for c in ctrpiù di una volta.
Curt

7
@Curt: Assolutamente no. Counterè un iteratore e gli iteratori dovrebbero essere ripetuti una sola volta. Se si ripristina self.currentin__iter__ , poi un ciclo nidificato sul Countersarebbe completamente rotto, e ogni sorta di comportamenti assunti di iteratori (che chiamare itersu di essi è idempotente) sono violati. Se vuoi essere in grado di iterare ctrpiù di una volta, deve essere un iterabile non iteratore, dove restituisce un iteratore nuovo ogni volta che __iter__viene invocato. Cercare di combinare (un iteratore che viene reimpostato implicitamente quando __iter__viene invocato) viola i protocolli.
ShadowRanger

2
Ad esempio, se Counterdovesse essere un iterabile non iteratore, rimuoveresti la definizione di __next__/ nextinteramente e probabilmente ridefiniresti __iter__come funzione generatore dello stesso modulo del generatore descritto alla fine di questa risposta (tranne invece che i limiti provenienti da argomenti __iter__, sarebbero argomenti su cui __init__salvare selfe ai quali si può accedereself a __iter__).
ShadowRanger

427

Esistono quattro modi per creare una funzione iterativa:

Esempi:

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

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

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

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

Per vedere tutti e quattro i metodi in azione:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
    for ch in iterator('abcde'):
        print(ch, end=' ')
    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 tipi di generatore ( uc_gene uc_genexp) non possono essere reversed(); il semplice iteratore ( uc_iter) avrebbe bisogno del __reversed__metodo magico (che, secondo i documenti , deve restituire un nuovo iteratore, ma restituendo le selfopere (almeno in CPython)); e getitem iteratable ( uc_getitem) deve avere il __len__metodo magico:

    # for uc_iter we add __reversed__ and update __next__
    def __reversed__(self):
        self.index = -1
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += -1 if self.index < 0 else +1
        return result

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

Per rispondere alla domanda secondaria del colonnello Panic su un iteratore infinitamente valutato pigramente, ecco questi esempi, usando ciascuno dei quattro metodi 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

Il risultato è (almeno per la mia esecuzione di esempio):

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

Come scegliere quale usare? Questa è principalmente una questione di gusti. I due metodi che vedo più spesso sono i generatori e il protocollo iteratore, nonché un ibrido (che __iter__restituisce un generatore).

Le espressioni del generatore sono utili per sostituire la comprensione dell'elenco (sono pigre e quindi possono risparmiare risorse).

Se si necessita di compatibilità con le versioni precedenti di Python 2.x, utilizzare __getitem__.


4
Mi piace questo riepilogo perché è completo. Questi tre modi (resa, espressione del generatore e iteratore) sono essenzialmente gli stessi, sebbene alcuni siano più convenienti di altri. L'operatore di rendimento acquisisce la "continuazione" che contiene lo stato (ad esempio l'indice che stiamo facendo). Le informazioni vengono salvate 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é si indicizza nei contenuti e non è iterativo in natura.
Ian,

2
@metaperl: In realtà lo è. In tutti e quattro i casi precedenti è possibile utilizzare lo stesso codice per iterare.
Ethan Furman,

1
@Asterisk: No, un'istanza di uc_iterdovrebbe scadere al termine (altrimenti sarebbe infinita); se vuoi farlo di nuovo devi ottenere un nuovo iteratore chiamando di uc_iter()nuovo.
Ethan Furman,

2
È possibile impostare self.index = 0in __iter__modo da poter ripetere più volte. Altrimenti non puoi.
John Strood,

1
Se potessi risparmiare tempo, apprezzerei una spiegazione del perché sceglieresti uno dei metodi rispetto agli altri.
aaaaaa,

103

Innanzitutto il modulo itertools è 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 Python:

dare la precedenza

Non è fantastico? Il rendimento può essere utilizzato per sostituire un rendimento normale in una funzione. Restituisce l'oggetto lo stesso, ma invece di distruggere lo stato e di uscire, salva lo stato per quando si desidera eseguire la successiva iterazione. Ecco un esempio in azione estratto direttamente dall'elenco delle funzioni di itertools :

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

Come indicato nella descrizione delle funzioni (è la funzione count () dal modulo itertools ...), produce un iteratore che restituisce numeri interi consecutivi a partire da n.

Le espressioni dei generatori sono tutta un'altra lattina di worm (fantastici worm!). Possono essere utilizzati al posto di un Comprensione elenco per risparmiare memoria (le comprensioni dell'elenco 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 di dicendo Iteratore). Ecco un esempio di definizione di un'espressione del generatore:

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

Questo è molto simile alla nostra definizione di iteratore sopra, tranne per il fatto che l'intera gamma è predeterminata tra 0 e 10.

Ho appena trovato xrange () (sorpreso di non averlo mai visto prima ...) e l'ho aggiunto all'esempio sopra. xrange () è una versione iterabile di range () che ha il vantaggio di non precompilare l'elenco. Sarebbe molto utile se avessi un corpus gigantesco di dati su cui scorrere e avessi solo tanta memoria per farlo.


20
a partire da Python 3.0 non esiste più un xrange () e il nuovo range () si comporta come il vecchio xrange ()

6
Dovresti comunque usare xrange in 2._, perché 2to3 lo traduce automaticamente.
Phob,

100

Vedo alcuni di voi che ci fanno return selfdentro __iter__. Volevo solo notare che esso __iter__stesso può essere un generatore (eliminando così la necessità __next__e sollevando StopIterationeccezioni)

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 creare direttamente un generatore, ma per classi più complesse può essere utile.


5
Grande! È così noioso scrivere solo return selfin __iter__. Quando stavo per provare ad usarlo yieldho trovato il tuo codice che faceva esattamente quello che volevo provare.
Ray

3
Ma in questo caso, come si implementerebbe next()? return iter(self).next()?
Lenna,

4
@Lenna, è già "implementato" perché iter (self) restituisce un iteratore, non un'istanza di intervallo.
Manux,

3
Questo è il modo più semplice per farlo, e non comporta la necessità di tenere traccia di eg self.currento di qualsiasi altro contatore. Questa dovrebbe essere la risposta più votata!
astrofrog

4
Per essere chiari, questo approccio rende iterabile la tua classe , ma non un iteratore . Ottieni iteratori nuovi ogni volta che inviti itera istanze della classe, ma non sono esse stesse istanze della classe.
ShadowRanger

13

Questa domanda riguarda gli oggetti iterabili, non gli iteratori. In Python, anche le sequenze sono iterabili, quindi un modo per creare una classe iterabile è farla comportare come una sequenza, ovvero dargli __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)

1
Non deve avere un __len__()metodo. __getitem__da solo con il comportamento previsto è sufficiente.
BlackJack,

5

Tutte le risposte in questa pagina sono davvero fantastiche per un oggetto complesso. Ma per quelli contenenti incorporato tipi iterabili come attributi, come str, list, seto dict, o di qualsiasi implementazione di collections.Iterable, è possibile omettere certe cose nella vostra classe.

class Test(object):
    def __init__(self, string):
        self.string = string

    def __iter__(self):
        # since your string is already iterable
        return (ch for ch in self.string)
        # or simply
        return self.string.__iter__()
        # also
        return iter(self.string)

Può essere usato come:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e

1
Come lei ha detto, la stringa è già così iterabile perché il generatore di espressione in più in mezzo invece di chiedere la stringa per l'iteratore (che il generatore di espressione fa internamente): return iter(self.string).
BlackJack,

@BlackJack Hai davvero ragione. Non so cosa mi abbia convinto a scrivere in quel modo. Forse stavo cercando di evitare qualsiasi confusione in una risposta cercando di spiegare il funzionamento della sintassi dell'iteratore in termini di più sintassi dell'iteratore.
John Strood,

3

Questa è una funzione iterabile senza yield. Fa uso della iterfunzione e di una chiusura che mantiene il suo stato in un mutable ( list) nell'ambito compreso in python 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 viene mantenuto immutabile nell'ambito compreso e nonlocalviene utilizzato nell'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

Apprezzo sempre un uso intelligente di two-arg iter, ma solo per essere chiari: è più complesso e meno efficiente del semplice utilizzo di una yieldfunzione di generatore basata; Python ha un sacco di supporto per interpreti per yieldfunzioni di generatore basate che non puoi sfruttare qui, rendendo questo codice significativamente più lento. Tuttavia votato.
ShadowRanger

2

Se stai cercando qualcosa di breve e semplice, forse ti basterà:

class A(object):
    def __init__(self, l):
        self.data = l

    def __iter__(self):
        return iter(self.data)

esempio di utilizzo:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

-1

Ispirato dalla risposta di Matt Gregory qui è un iteratore un po 'più complicato che restituirà a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz

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

    def __iter__(self):
        return self

    def __next__(self): # Python 3: def __next__(self)
        alpha = ' abcdefghijklmnopqrstuvwxyz'
        n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
        n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
        if n_current > n_high:
            raise StopIteration
        else:
            increment = True
            ret = ''
            for x in self.current[::-1]:
                if 'z' == x:
                    if increment:
                        ret += 'a'
                    else:
                        ret += 'z'
                else:
                    if increment:
                        ret += alpha[alpha.find(x)+1]
                        increment = False
                    else:
                        ret += x
            if increment:
                ret += 'a'
            tmp = self.current
            self.current = ret[::-1]
            return tmp

for c in AlphaCounter('a', 'zzz'):
    print(c)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.