Eu gostaria de esclarecer um pouco mais de luz sobre a interação de iter, __iter__e __getitem__eo que acontece por trás das cortinas. Armado com esse conhecimento, você será capaz de entender por que o melhor que você pode fazer é
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Vou listar os fatos primeiro e depois acompanhar com um lembrete rápido do que acontece quando você emprega um forloop em python, seguido de uma discussão para ilustrar os fatos.
Fatos
Você pode obter um iterador de qualquer objeto ochamando iter(o)se pelo menos uma das seguintes condições for verdadeira:
a) opossui um __iter__método que retorna um objeto iterador. Um iterador é qualquer objeto com um método __iter__e __next__(Python 2 next:).
b) otem um __getitem__método
Verificar uma instância de Iterableou Sequenceou verificar o atributo __iter__não é suficiente.
Se um objeto oimplementar apenas __getitem__, mas não __iter__, iter(o)construirá um iterador que tenta buscar itens opelo índice inteiro, iniciando no índice 0. O iterador capturará qualquer IndexError(mas nenhum outro erro) gerado e, em seguida, se elevará StopIteration.
No sentido mais geral, não há como verificar se o iterador retornado iteré sensato, exceto para testá-lo.
Se um objeto for oimplementado __iter__, a iterfunção garantirá que o objeto retornado por __iter__seja um iterador. Não há verificação de integridade se um objeto é implementado apenas __getitem__.
__iter__vitórias. Se um objeto oimplementa ambos __iter__e __getitem__, iter(o)será chamado __iter__.
Se você deseja tornar seus próprios objetos iteráveis, sempre implemente o __iter__método
for rotações
Para acompanhar, você precisa entender o que acontece quando você emprega um forloop no Python. Sinta-se livre para pular para a próxima seção, se você já sabe.
Quando você usa for item in opara algum objeto iterável o, o Python chama iter(o)e espera um objeto iterador como o valor de retorno. Um iterador é qualquer objeto que implementa um método __next__(ou nextno Python 2) e um __iter__método.
Por convenção, o __iter__método de um iterador deve retornar o próprio objeto (ou seja return self). O Python então chama nexto iterador até que StopIterationseja gerado. Tudo isso acontece implicitamente, mas a seguinte demonstração o torna visível:
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
Iteração sobre um 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
Discussão e ilustrações
Nos pontos 1 e 2: obtendo um iterador e verificações não confiáveis
Considere a seguinte classe:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Chamar itercom uma instância de BasicIterableretornará um iterador sem problemas, porque BasicIterableimplementa __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
No entanto, é importante observar que bnão possui o __iter__atributo e não é considerado uma instância Iterableou Sequence:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
É por isso que o Fluent Python de Luciano Ramalho recomenda chamar itere manipular o potencial TypeErrorcomo a maneira mais precisa de verificar se um objeto é iterável. Citando diretamente do livro:
No Python 3.4, a maneira mais precisa de verificar se um objeto xé iterável é chamar iter(x)e manipular uma TypeErrorexceção, se não for. Isso é mais preciso do que usar isinstance(x, abc.Iterable), porque iter(x)também considera o __getitem__método legado , enquanto o IterableABC não.
No ponto 3: Iterando sobre objetos que fornecem apenas __getitem__, mas não__iter__
Iterando sobre uma instância de BasicIterableobras conforme o esperado: Python constrói um iterador que tenta buscar itens por índice, começando em zero, até que um IndexErrorseja gerado. O __getitem__método do objeto demo simplesmente retorna o itemque foi fornecido como argumento __getitem__(self, item)pelo iterador retornado por 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
Observe que o iterador dispara StopIterationquando não pode retornar o próximo item e que o IndexErrorque é levantado item == 3é tratado internamente. É por isso que repetir BasicIterableum forloop com um loop funciona conforme o esperado:
>>> for x in b:
... print(x)
...
0
1
2
Aqui está outro exemplo para esclarecer o conceito de como o iterador retornou itertentando acessar itens por índice. WrappedDictnão herda dict, o que significa que as instâncias não terão um __iter__método.
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
Observe que as chamadas para __getitem__são delegadas dict.__getitem__para as quais a notação de colchete é simplesmente uma abreviação.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Nos pontos 4 e 5: iterverifica um iterador quando ele chama__iter__ :
Quando iter(o)é chamado para um objeto o, itergarante que o valor de retorno de __iter__, se o método estiver presente, seja um iterador. Isso significa que o objeto retornado deve implementar __next__(ou nextno Python 2) e __iter__. iternão pode executar nenhuma verificação de integridade para objetos que apenas fornecem __getitem__, porque não há como verificar se os itens do objeto estão acessíveis pelo índice inteiro.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Observe que a construção de um iterador a partir de FailIterIterableinstâncias falha imediatamente, enquanto a construção de um iterador FailGetItemIterableé bem - sucedida, mas lança uma exceção na primeira chamada para __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
No ponto 6: __iter__vitórias
Este é direto. Se um objeto implementa __iter__e __getitem__, iterserá chamado __iter__. Considere a seguinte classe
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
e a saída ao fazer um loop sobre uma instância:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
No ponto 7: suas classes iteráveis devem implementar __iter__
Você pode se perguntar por que a maioria das seqüências internas, como listimplementar um __iter__método, __getitem__seria suficiente.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Afinal, iteração sobre as instâncias da classe acima, que delega chamadas para __getitem__a list.__getitem__(usando a notação colchete), vai funcionar bem:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Os motivos pelos quais os iterables personalizados devem implementar __iter__são os seguintes:
- Se você implementar
__iter__, as instâncias serão consideradas iteráveis e isinstance(o, collections.abc.Iterable)retornarão True.
- Se o objeto retornado por
__iter__não for um iterador, iterfalhará imediatamente e aumentará a TypeError.
- O tratamento especial de
__getitem__existe por motivos de compatibilidade com versões anteriores. Citando novamente o Fluent Python:
É por isso que qualquer sequência Python é iterável: todas elas são implementadas __getitem__. De fato, as seqüências padrão também são implementadas __iter__, e a sua também, porque o tratamento especial de __getitem__existe por razões de compatibilidade com versões anteriores e pode desaparecer no futuro (embora não seja preterido enquanto escrevo isso).
__getitem__Também é suficiente para fazer um objeto iterável