Mi piacerebbe gettare un po 'di più in luce l'interazione di iter
, __iter__
ed __getitem__
e ciò che accade dietro le tende. Grazie a questa conoscenza, sarai in grado di capire perché il meglio che puoi fare è
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Elencherò prima i fatti e poi seguirò con un breve promemoria di ciò che accade quando si utilizza un for
loop in Python, seguito da una discussione per illustrare i fatti.
I fatti
Puoi ottenere un iteratore da qualsiasi oggetto o
chiamando iter(o)
se almeno una delle seguenti condizioni è vera:
a) o
ha un __iter__
metodo che restituisce un oggetto iteratore. Un iteratore è qualsiasi oggetto con un __iter__
e un metodo __next__
(Python 2 next
:).
b) o
ha un __getitem__
metodo.
Controllare un'istanza di Iterable
o Sequence
oppure verificare l'attributo __iter__
non è sufficiente.
Se un oggetto o
implementa solo __getitem__
, ma non __iter__
, iter(o)
costruirà un iteratore che tenta di recuperare elementi o
dall'indice intero, iniziando dall'indice 0. L'iteratore catturerà qualsiasi IndexError
(ma nessun altro errore) che viene generato e quindi si solleva StopIteration
.
Nel senso più generale, non c'è modo di verificare se l'iteratore restituito iter
è sano se non quello di provarlo.
Se un oggetto viene o
implementato __iter__
, la iter
funzione assicurerà che l'oggetto restituito __iter__
sia un iteratore. Non esiste un controllo di integrità se un oggetto implementa solo __getitem__
.
__iter__
vince. Se un oggetto o
implementa entrambi __iter__
e __getitem__
, iter(o)
chiamerà __iter__
.
Se si desidera rendere iterabili i propri oggetti, implementare sempre il __iter__
metodo
for
loop
Per poter seguire, è necessario comprendere cosa succede quando si utilizza un for
loop in Python. Sentiti libero di saltare direttamente alla sezione successiva se lo sai già.
Quando si utilizza for item in o
per qualche oggetto iterabile o
, Python chiama iter(o)
e si aspetta un oggetto iteratore come valore restituito. Un iteratore è qualsiasi oggetto che implementa un metodo __next__
(o next
in Python 2) e un __iter__
metodo.
Per convenzione, il __iter__
metodo di un iteratore dovrebbe restituire l'oggetto stesso (cioè return self
). Python quindi chiama next
l'iteratore fino a quando non StopIteration
viene generato. Tutto ciò accade implicitamente, ma la seguente dimostrazione lo rende visibile:
import random
class DemoIterable(object):
def __iter__(self):
print('__iter__ called')
return DemoIterator()
class DemoIterator(object):
def __iter__(self):
return self
def __next__(self):
print('__next__ called')
r = random.randint(1, 10)
if r == 5:
print('raising StopIteration')
raise StopIteration
return r
Iterazione su un DemoIterable
:
>>> di = DemoIterable()
>>> for x in di:
... print(x)
...
__iter__ called
__next__ called
9
__next__ called
8
__next__ called
10
__next__ called
3
__next__ called
10
__next__ called
raising StopIteration
Discussione e illustrazioni
Sui punti 1 e 2: ottenere un iteratore e controlli inaffidabili
Considera la seguente classe:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Chiamare iter
con un'istanza di BasicIterable
restituirà un iteratore senza problemi perché BasicIterable
implementa __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Tuttavia, è importante notare che b
non ha l' __iter__
attributo e non è considerato un'istanza di Iterable
o Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Ecco perché Fluent Python di Luciano Ramalho raccomanda di chiamare iter
e gestire il potenziale TypeError
come il modo più accurato per verificare se un oggetto è iterabile. Citando direttamente dal libro:
A partire da Python 3.4, il modo più accurato per verificare se un oggetto x
è iterabile è chiamare iter(x)
e gestire TypeError
un'eccezione se non lo è. Questo è più preciso dell'uso isinstance(x, abc.Iterable)
, perché iter(x)
considera anche il __getitem__
metodo legacy , mentre l' Iterable
ABC no.
Al punto 3: Iterazione su oggetti che forniscono solo __getitem__
, ma non__iter__
Iterazione su un'istanza di BasicIterable
lavori come previsto: Python costruisce un iteratore che tenta di recuperare gli elementi per indice, iniziando da zero, fino a quando non IndexError
viene generato un. Il __getitem__
metodo dell'oggetto demo restituisce semplicemente ciò item
che è stato fornito come argomento __getitem__(self, item)
dall'iteratore restituito da iter
.
>>> b = BasicIterable()
>>> it = iter(b)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Si noti che l'iteratore aumenta StopIteration
quando non è in grado di restituire l'elemento successivo e quello per IndexError
cui viene sollevato item == 3
viene gestito internamente. Questo è il motivo per cui il looping su a BasicIterable
con un for
loop funziona come previsto:
>>> for x in b:
... print(x)
...
0
1
2
Ecco un altro esempio per portare a casa il concetto di come l'iteratore restituito iter
tenta di accedere agli elementi per indice. WrappedDict
non eredita da dict
, il che significa che le istanze non avranno un __iter__
metodo.
class WrappedDict(object): # note: no inheritance from dict!
def __init__(self, dic):
self._dict = dic
def __getitem__(self, item):
try:
return self._dict[item] # delegate to dict.__getitem__
except KeyError:
raise IndexError
Si noti che le chiamate a __getitem__
vengono delegate dict.__getitem__
per le quali la notazione tra parentesi quadre è semplicemente una scorciatoia.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Ai punti 4 e 5: iter
verifica la presenza di un iteratore quando chiama__iter__
:
Quando iter(o)
viene chiamato per un oggetto o
, iter
si assicurerà che il valore restituito di __iter__
, se il metodo è presente, sia un iteratore. Ciò significa che l'oggetto restituito deve essere implementato __next__
(o next
in Python 2) e __iter__
. iter
non può eseguire alcun controllo di integrità per gli oggetti che forniscono solo __getitem__
, poiché non ha modo di verificare se gli oggetti dell'oggetto sono accessibili tramite l'indice intero.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Si noti che la costruzione di un iteratore dalle FailIterIterable
istanze non riesce immediatamente, mentre la costruzione di un iteratore ha esito FailGetItemIterable
positivo, ma genererà un'eccezione alla prima chiamata a __next__
.
>>> fii = FailIterIterable()
>>> iter(fii)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'object'
>>>
>>> fgi = FailGetitemIterable()
>>> it = iter(fgi)
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/path/iterdemo.py", line 42, in __getitem__
raise Exception
Exception
Al punto 6: __iter__
vince
Questo è semplice. Se un oggetto implementa __iter__
e __getitem__
, iter
chiamerà __iter__
. Considera la seguente classe
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
e l'output durante il loop su un'istanza:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
Al punto 7: le tue classi iterabili dovrebbero essere implementate __iter__
Potresti chiederti perché la maggior parte delle sequenze integrate come list
implementare un __iter__
metodo quando __getitem__
sarebbe sufficiente.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Dopotutto, l'iterazione sulle istanze della classe sopra, che i delegati chiamano __getitem__
a list.__getitem__
(usando la notazione con parentesi quadra), funzionerà bene:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
I motivi per cui i tuoi iterable personalizzati devono essere implementati __iter__
sono i seguenti:
- Se si implementa
__iter__
, le istanze verranno considerate iterabili e isinstance(o, collections.abc.Iterable)
verranno restituite True
.
- Se l'oggetto restituito da
__iter__
non è un iteratore, iter
fallirà immediatamente e genererà a TypeError
.
- La gestione speciale di
__getitem__
esiste per motivi di compatibilità con le versioni precedenti. Citando di nuovo da Fluent Python:
Ecco perché qualsiasi sequenza di Python è iterabile: tutti implementano __getitem__
. In effetti, anche le sequenze standard implementano __iter__
, e anche le tue, perché la gestione speciale di __getitem__
esiste per motivi di compatibilità con le versioni precedenti e potrebbe essere andata in futuro (anche se non è deprecata mentre scrivo questo).
__getitem__
è anche sufficiente per rendere un oggetto iterabile