Respostas:
Objetos iteradores em python estão em conformidade com o protocolo iterador, o que basicamente significa que eles fornecem dois métodos: __iter__()
e __next__()
.
O __iter__
retorna o objeto iterador e é chamado implicitamente no início dos loops.
O __next__()
método retorna o próximo valor e é chamado implicitamente a cada incremento de loop. Esse método gera uma exceção StopIteration quando não há mais valor a ser retornado, que é capturado implicitamente por construções em loop para interromper a iteração.
Aqui está um exemplo simples de um contador:
class Counter:
def __init__(self, low, high):
self.current = low - 1
self.high = high
def __iter__(self):
return self
def __next__(self): # Python 2: def next(self)
self.current += 1
if self.current < self.high:
return self.current
raise StopIteration
for c in Counter(3, 9):
print(c)
Isso imprimirá:
3
4
5
6
7
8
É mais fácil escrever usando um gerador, conforme abordado em uma resposta anterior:
def counter(low, high):
current = low
while current < high:
yield current
current += 1
for c in counter(3, 9):
print(c)
A saída impressa será a mesma. Sob o capô, o objeto gerador suporta o protocolo iterador e faz algo aproximadamente semelhante à classe Counter.
O artigo de David Mertz, Iteradores e Geradores Simples , é uma introdução muito boa.
__next__
. counter
é um iterador, mas não é uma sequência. Não armazena seus valores. Você não deve usar o contador em um loop for duplamente aninhado, por exemplo.
__iter__
(além de em __init__
). Caso contrário, o objeto poderá ser iterado apenas uma vez. Por exemplo, se você diz ctr = Counters(3, 8)
, não pode usar for c in ctr
mais de uma vez.
Counter
é um iterador, e iteradores devem ser iterados apenas uma vez. Se você redefinir self.current
em __iter__
, em seguida, um loop aninhado sobre o Counter
seria completamente quebrado, e todos os tipos de comportamentos assumidos de iterators (que chamar iter
sobre eles é idempotent) são violados. Se você deseja iterar ctr
mais de uma vez, ele precisa ser iterável não iterador, onde retorna um iterador novinho em folha cada vez que __iter__
é invocado. Tentar misturar e combinar (um iterador que é implicitamente redefinido quando __iter__
invocado) viola os protocolos.
Counter
fosse iterável para não iterador, você removeria a definição de __next__
/ next
inteiramente e provavelmente redefiniria __iter__
como uma função de gerador da mesma forma que o gerador descrito no final desta resposta (exceto em vez dos limites) vindo de argumentos para __iter__
, eles seriam argumentos para serem __init__
salvos self
e acessados de self
dentro __iter__
).
Existem quatro maneiras de criar uma função iterativa:
__iter__
e__next__
(ou next
no Python 2.x))__getitem__
)Exemplos:
# generator
def uc_gen(text):
for char in text.upper():
yield char
# generator expression
def uc_genexp(text):
return (char for char in text.upper())
# iterator protocol
class uc_iter():
def __init__(self, text):
self.text = text.upper()
self.index = 0
def __iter__(self):
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
# getitem method
class uc_getitem():
def __init__(self, text):
self.text = text.upper()
def __getitem__(self, index):
return self.text[index]
Para ver todos os quatro métodos em ação:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
for ch in iterator('abcde'):
print(ch, end=' ')
print()
O que resulta em:
A B C D E
A B C D E
A B C D E
A B C D E
Nota :
Os dois tipos de gerador ( uc_gen
e uc_genexp
) não podem ser reversed()
; o iterador simples ( uc_iter
) precisaria do __reversed__
método mágico (que, de acordo com os documentos , deve retornar um novo iterador, mas o retorno self
funciona (pelo menos no CPython)); e o getitem iteratable ( uc_getitem
) deve ter o __len__
método mágico:
# for uc_iter we add __reversed__ and update __next__
def __reversed__(self):
self.index = -1
return self
def __next__(self):
try:
result = self.text[self.index]
except IndexError:
raise StopIteration
self.index += -1 if self.index < 0 else +1
return result
# for uc_getitem
def __len__(self)
return len(self.text)
Para responder à pergunta secundária do coronel Panic sobre um iterador infinito avaliado preguiçosamente, aqui estão esses exemplos, usando cada um dos quatro métodos acima:
# 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
O que resulta em (pelo menos para a minha amostra):
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
Como escolher qual usar? Isso é principalmente uma questão de gosto. Os dois métodos que vejo com mais freqüência são geradores e o protocolo iterador, além de um híbrido ( __iter__
retornando um gerador).
As expressões do gerador são úteis para substituir as compreensões da lista (são preguiçosas e podem economizar recursos).
Se for necessário compatibilidade com versões anteriores do Python 2.x, use __getitem__
.
uc_iter
deve expirar quando terminar (caso contrário, seria infinita); se você quiser fazer isso novamente, precisará obter um novo iterador ligando uc_iter()
novamente.
self.index = 0
em __iter__
para que você possa interagir muitas vezes. Caso contrário, você não pode.
Primeiro de tudo, o módulo itertools é incrivelmente útil para todos os tipos de casos em que um iterador seria útil, mas aqui é tudo o que você precisa para criar um iterador em python:
produção
Isso não é legal? O rendimento pode ser usado para substituir um retorno normal em uma função. Ele retorna o objeto da mesma forma, mas em vez de destruir o estado e sair, ele salva o estado para quando você deseja executar a próxima iteração. Aqui está um exemplo disso em ação, extraído diretamente da lista de funções do itertools :
def count(n=0):
while True:
yield n
n += 1
Conforme indicado na descrição das funções (é a função count () do módulo itertools ...), produz um iterador que retorna números inteiros consecutivos começando com n.
As expressões de gerador são uma outra lata de worms (worms impressionantes!). Eles podem ser usados no lugar de uma Compreensão de lista para economizar memória (as compreensões de lista criam uma lista na memória que é destruída após o uso, se não for atribuída a uma variável, mas as expressões geradoras podem criar um Objeto Gerador ... que é uma maneira elegante de dizendo Iterador). Aqui está um exemplo de uma definição de expressão do gerador:
gen = (n for n in xrange(0,11))
Isso é muito semelhante à nossa definição de iterador acima, exceto que o intervalo completo é predeterminado entre 0 e 10.
Acabei de encontrar xrange () (surpreso por não ter visto isso antes ...) e o adicionei ao exemplo acima. xrange () é uma versão iterável do range () que tem a vantagem de não pré-construir a lista. Seria muito útil se você tivesse um corpus gigante de dados para iterar e tivesse apenas muita memória para fazer isso.
Vejo alguns de vocês fazendo return self
em __iter__
. Eu só queria observar que __iter__
ele próprio pode ser um gerador (removendo assim a necessidade __next__
e criando StopIteration
exceções)
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
É claro que aqui é possível criar diretamente um gerador, mas para classes mais complexas, pode ser útil.
return self
em __iter__
. Quando eu tentava usá yield
-lo, encontrei seu código fazendo exatamente o que eu queria tentar.
next()
? return iter(self).next()
?
self.current
ou qualquer outro contador. Essa deve ser a resposta mais votada!
iter
instâncias da classe, mas elas não são elas próprias instâncias da classe.
Esta pergunta é sobre objetos iteráveis, não sobre iteradores. No Python, as seqüências também são iteráveis; portanto, uma maneira de criar uma classe iterável é fazê-la se comportar como uma sequência, ou seja, fornecer a ela __getitem__
e __len__
métodos. Eu testei isso no 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)
__len__()
método. __getitem__
sozinho com o comportamento esperado é suficiente.
Todas as respostas nesta página são realmente ótimas para um objeto complexo. Mas para aqueles que contêm embutido tipos iteráveis como atributos, como str
, list
, set
ou dict
, ou em qualquer implementação de collections.Iterable
, você pode omitir certas coisas em sua 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 self.string)
# or simply
return self.string.__iter__()
# also
return iter(self.string)
Pode ser usado como:
for x in Test("abcde"):
print(x)
# prints
# a
# b
# c
# d
# e
return iter(self.string)
.
Esta é uma função iterável sem yield
. Ele faz uso da iter
função e de um fechamento que mantém seu estado em um mutable ( list
) no escopo do 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)
Para Python 3, o estado de fechamento é mantido em um imutável no escopo anexo e nonlocal
é usado no escopo local para atualizar a variável de estado.
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)
Teste;
for i in count(1,10):
print(i)
1
2
3
4
5
6
7
8
9
iter
, mas apenas para esclarecer: isso é mais complexo e menos eficiente do que apenas usar uma yield
função geradora baseada; O Python possui um monte de suporte de intérprete para yield
funções de gerador baseadas das quais você não pode tirar proveito daqui, tornando esse código significativamente mais lento. Mesmo votado.
Se você procura algo curto e simples, talvez seja o suficiente para você:
class A(object):
def __init__(self, l):
self.data = l
def __iter__(self):
return iter(self.data)
exemplo de uso:
In [3]: a = A([2,3,4])
In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Inspirado pela resposta de Matt Gregory, aqui está um iterador um pouco mais complicado que retornará 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)