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 forloop in Python, seguito da una discussione per illustrare i fatti.
I fatti
Puoi ottenere un iteratore da qualsiasi oggetto ochiamando iter(o)se almeno una delle seguenti condizioni è vera:
a) oha un __iter__metodo che restituisce un oggetto iteratore. Un iteratore è qualsiasi oggetto con un __iter__e un metodo __next__(Python 2 next:).
b) oha un __getitem__metodo.
Controllare un'istanza di Iterableo Sequenceoppure verificare l'attributo __iter__non è sufficiente.
Se un oggetto oimplementa solo __getitem__, ma non __iter__, iter(o)costruirà un iteratore che tenta di recuperare elementi odall'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 oimplementato __iter__, la iterfunzione 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 oimplementa 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 forloop in Python. Sentiti libero di saltare direttamente alla sezione successiva se lo sai già.
Quando si utilizza for item in oper 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 nextin 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 nextl'iteratore fino a quando non StopIterationviene 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 itercon un'istanza di BasicIterablerestituirà un iteratore senza problemi perché BasicIterableimplementa __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Tuttavia, è importante notare che bnon ha l' __iter__attributo e non è considerato un'istanza di Iterableo 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 itere gestire il potenziale TypeErrorcome 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 TypeErrorun'eccezione se non lo è. Questo è più preciso dell'uso isinstance(x, abc.Iterable), perché iter(x)considera anche il __getitem__metodo legacy , mentre l' IterableABC no.
Al punto 3: Iterazione su oggetti che forniscono solo __getitem__, ma non__iter__
Iterazione su un'istanza di BasicIterablelavori come previsto: Python costruisce un iteratore che tenta di recuperare gli elementi per indice, iniziando da zero, fino a quando non IndexErrorviene generato un. Il __getitem__metodo dell'oggetto demo restituisce semplicemente ciò itemche è 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 StopIterationquando non è in grado di restituire l'elemento successivo e quello per IndexErrorcui viene sollevato item == 3viene gestito internamente. Questo è il motivo per cui il looping su a BasicIterablecon un forloop 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 itertenta di accedere agli elementi per indice. WrappedDictnon 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: iterverifica la presenza di un iteratore quando chiama__iter__ :
Quando iter(o)viene chiamato per un oggetto o, itersi 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 nextin Python 2) e __iter__. iternon 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 FailIterIterableistanze non riesce immediatamente, mentre la costruzione di un iteratore ha esito FailGetItemIterablepositivo, 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__, iterchiamerà __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 listimplementare 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, iterfallirà 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