Costruisci un iteratore Python di base
Domanda
Come si creerebbe una funzione iterativa (o oggetto iteratore) in Python?
Soluzione
Gli oggetti iteratori in Python sono conformi al protocollo iteratore, il che significa sostanzialmente che forniscono due metodi: __iter__()
E next()
.IL __iter__
restituisce l'oggetto iteratore e viene chiamato implicitamente all'inizio dei cicli.IL next()
Il metodo 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 implicitamente acquisita eseguendo il looping dei costrutti per interrompere l'iterazione.
Ecco un semplice esempio di 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 stamperà:
3
4
5
6
7
8
Questo è più semplice da scrivere utilizzando 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, 8):
print c
L'output stampato sarà lo stesso.Dietro le quinte, l'oggetto generatore supporta il protocollo iteratore e fa qualcosa di più o meno simile alla classe Counter.
L'articolo di David Mertz, Iteratori e generatori semplici, è un'ottima introduzione.
Altri suggerimenti
Esistono quattro modi per costruire una funzione iterativa:
- creare un generatore (utilizza il file parola chiave rendimento)
- utilizzare un'espressione generatrice (genexp)
- creare un iteratore (definisce
__iter__
E__next__
(Onext
in Python 2.x)) - creare una classe su cui Python possa eseguire l'iterazione da sola (definisce
__getitem__
)
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 e quattro i metodi in azione:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
for ch in iterator('abcde'):
print ch,
print
Il 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_gen
E uc_genexp
) non può essere reversed()
;il semplice iteratore (uc_iter
) avrebbe bisogno di __reversed__
metodo magic (che deve restituire un nuovo iteratore che va all'indietro);e il getitem iterabile (uc_getitem
) deve avere il __len__
metodo magico:
# for uc_iter
def __reversed__(self):
return reversed(self.text)
# for uc_getitem
def __len__(self)
return len(self.text)
Per rispondere alla domanda secondaria del colonnello Panic su un iteratore infinito valutato pigramente, ecco questi esempi, utilizzando ciascuno dei quattro metodi sopra indicati:
# 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 che si traduce in (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
Innanzitutto il modulo itertools è incredibilmente utile per tutti i tipi di casi in cui un iteratore sarebbe utile, ma ecco tutto ciò che serve per creare un iteratore in Python:
prodotto
Non è bello?La resa può essere utilizzata per sostituire una normale ritorno in una funzione.Restituisce l'oggetto lo stesso, ma invece di distruggere lo stato ed uscire, salva lo stato per quando si desidera eseguire l'iterazione successiva.Eccone un esempio in azione estratto direttamente dal file Elenco delle funzioni di itertools:
def count(n=0):
while True:
yield n
n += 1
Come indicato nella descrizione delle funzioni (è il file contare() funzione dal modulo itertools...), produce un iteratore che restituisce numeri interi consecutivi che iniziano con n.
Espressioni del generatore sono tutta un'altra scatola di vermi (vermi fantastici!).Possono essere utilizzati al posto di a Comprensione delle liste per risparmiare memoria (le comprensioni delle liste creano una lista in memoria che viene distrutta dopo l'uso se non assegnata 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 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'intervallo completo è predeterminato tra 0 e 10.
Ho appena trovato xrange() (sorpreso di non averlo visto prima...) e l'ho aggiunto all'esempio sopra. xrange() è una versione iterabile di allineare() che ha il vantaggio di non precostruire l'elenco.Sarebbe molto utile se avessi un enorme corpus di dati su cui eseguire l'iterazione e avessi solo tanta memoria per farlo.
Vedo che alcuni di voi lo fanno return self
In __iter__
.Volevo solo sottolinearlo __iter__
stesso può essere un generatore (eliminando così la necessità di __next__
e innalzamento 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 realizzare direttamente un generatore, ma per classi più complesse può essere utile.
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, cioèdaglielo __getitem__
E __len__
metodi.L'ho testato 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)
Questa è una funzione iterabile senza yield
.Si avvale del iter
funzione e una chiusura che mantiene il suo stato in un formato mutabile (list
) nell'ambito allegato per 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 è mantenuto immutabile nell'ambito di inclusione e nonlocal
viene 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
Tutte le risposte in questa pagina sono davvero ottime per un oggetto complesso.Ma per quelli che contengono tipi iterabili incorporati come attributi, come str
, list
, set
O dict
, o qualsiasi implementazione di collections.Iterable
, puoi omettere alcune cose nella tua 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 string)
Può essere utilizzato come:
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
Se cerchi 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]
Ispirato dalla risposta di Matt Gregory, ecco 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)