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 for
loop em python, seguido de uma discussão para ilustrar os fatos.
Fatos
Você pode obter um iterador de qualquer objeto o
chamando iter(o)
se pelo menos uma das seguintes condições for verdadeira:
a) o
possui um __iter__
método que retorna um objeto iterador. Um iterador é qualquer objeto com um método __iter__
e __next__
(Python 2 next
:).
b) o
tem um __getitem__
método
Verificar uma instância de Iterable
ou Sequence
ou verificar o atributo __iter__
não é suficiente.
Se um objeto o
implementar apenas __getitem__
, mas não __iter__
, iter(o)
construirá um iterador que tenta buscar itens o
pelo í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 o
implementado __iter__
, a iter
funçã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 o
implementa 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 for
loop no Python. Sinta-se livre para pular para a próxima seção, se você já sabe.
Quando você usa for item in o
para 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 next
no 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 next
o iterador até que StopIteration
seja 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 iter
com uma instância de BasicIterable
retornará um iterador sem problemas, porque BasicIterable
implementa __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
No entanto, é importante observar que b
não possui o __iter__
atributo e não é considerado uma instância Iterable
ou 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 iter
e manipular o potencial TypeError
como 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 TypeError
exceçã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 Iterable
ABC não.
No ponto 3: Iterando sobre objetos que fornecem apenas __getitem__
, mas não__iter__
Iterando sobre uma instância de BasicIterable
obras conforme o esperado: Python constrói um iterador que tenta buscar itens por índice, começando em zero, até que um IndexError
seja gerado. O __getitem__
método do objeto demo simplesmente retorna o item
que 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 StopIteration
quando não pode retornar o próximo item e que o IndexError
que é levantado item == 3
é tratado internamente. É por isso que repetir BasicIterable
um for
loop 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 iter
tentando acessar itens por índice. WrappedDict
nã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: iter
verifica um iterador quando ele chama__iter__
:
Quando iter(o)
é chamado para um objeto o
, iter
garante 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 next
no Python 2) e __iter__
. iter
nã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 FailIterIterable
instâ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__
, iter
será 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 list
implementar 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, iter
falhará 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