Crie um iterador básico do Python

569

Como alguém criaria uma função iterativa (ou objeto iterador) em python?

Akdom
fonte

Respostas:

650

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.

ars
fonte
4
Essa é principalmente uma boa resposta, mas o fato de ela retornar a si mesma é um pouco abaixo do ideal. Por exemplo, se você usasse o mesmo objeto de contador em um loop for duplamente aninhado, provavelmente não obteria o comportamento pretendido.
Casey Rodarmor
22
Não, os iteradores DEVEM retornar a si mesmos. Os iteráveis ​​retornam iteradores, mas não devem ser implementados __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.
leewz
4
No exemplo do contador, self.current deve ser atribuído em __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 ctrmais de uma vez.
Curt
7
@ Cort: Absolutamente não. Counteré um iterador, e iteradores devem ser iterados apenas uma vez. Se você redefinir self.currentem __iter__, em seguida, um loop aninhado sobre o Counterseria completamente quebrado, e todos os tipos de comportamentos assumidos de iterators (que chamar itersobre eles é idempotent) são violados. Se você deseja iterar ctrmais 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.
ShadowRanger 24/02
2
Por exemplo, se Counterfosse iterável para não iterador, você removeria a definição de __next__/ nextinteiramente 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 selfe acessados ​​de selfdentro __iter__).
ShadowRanger 24/02
427

Existem quatro maneiras de criar uma função iterativa:

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_gene 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 selffunciona (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__.

Ethan Furman
fonte
4
Eu gosto deste resumo porque está completo. Essas três maneiras (rendimento, expressão do gerador e iterador) são essencialmente as mesmas, embora algumas sejam mais convenientes que outras. O operador de rendimento captura a "continuação" que contém o estado (por exemplo, o índice que estamos fazendo). As informações são salvas no "encerramento" da continuação. A maneira do iterador salva as mesmas informações dentro dos campos do iterador, que são essencialmente a mesma coisa que um fechamento. O método getitem é um pouco diferente porque indexa o conteúdo e não é iterativo por natureza.
Ian
2
@metaperl: Na verdade, é. Nos quatro casos acima, você pode usar o mesmo código para iterar.
precisa
1
@ Aststerisk: Não, uma instância de uc_iterdeve expirar quando terminar (caso contrário, seria infinita); se você quiser fazer isso novamente, precisará obter um novo iterador ligando uc_iter()novamente.
21718 Ethan Furman
2
Você pode definir self.index = 0em __iter__para que você possa interagir muitas vezes. Caso contrário, você não pode.
19419 John Strood
1
Se você pudesse poupar tempo, agradeceria uma explicação sobre por que você escolheria um dos métodos em detrimento dos outros.
aaaaaa
103

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.

Akdom
fonte
20
como do pitão 3,0 não há mais uma xrange () e o novo intervalo () comporta-se como o velho xrange ()
6
Você ainda deve usar xrange em 2._, porque 2to3 o converte automaticamente.
Phob
100

Vejo alguns de vocês fazendo return selfem __iter__. Eu só queria observar que __iter__ele próprio pode ser um gerador (removendo assim a necessidade __next__e criando StopIterationexceçõ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.

Manux
fonte
5
Ótimo! É tão chato escrita apenas return selfem __iter__. Quando eu tentava usá yield-lo, encontrei seu código fazendo exatamente o que eu queria tentar.
Raio
3
Mas neste caso, como se implementaria next()? return iter(self).next()?
Lenna
4
@Lenna, ele já está "implementado" porque iter (self) retorna um iterador, não uma instância de intervalo.
precisa saber é
3
Essa é a maneira mais fácil de fazer isso, e não envolve ter que acompanhar, por exemplo, self.currentou qualquer outro contador. Essa deve ser a resposta mais votada!
astrofrog
4
Para ser claro, essa abordagem torna sua classe iterável , mas não um iterador . Você obtém iteradores novos toda vez que chama iterinstâncias da classe, mas elas não são elas próprias instâncias da classe.
ShadowRanger 24/02
13

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)
aq2
fonte
1
Não precisa ter um __len__()método. __getitem__sozinho com o comportamento esperado é suficiente.
BlackJack
5

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, setou 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
John Strood
fonte
1
Como você disse, a cadeia já é tão iterable porque a expressão gerador extra entre em vez de apenas pedir a corda para o iterador (que a expressão gerador faz internamente): return iter(self.string).
BlackJack
@BlackJack Você está realmente certo. Não sei o que me convenceu a escrever dessa maneira. Talvez eu estivesse tentando evitar qualquer confusão em uma resposta tentando explicar o funcionamento da sintaxe do iterador em termos de mais sintaxe do iterador.
John Strood
3

Esta é uma função iterável sem yield. Ele faz uso da iterfunçã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
Nizam Mohamed
fonte
Eu sempre aprecio o uso inteligente de dois argumentos iter, mas apenas para esclarecer: isso é mais complexo e menos eficiente do que apenas usar uma yieldfunção geradora baseada; O Python possui um monte de suporte de intérprete para yieldfunções de gerador baseadas das quais você não pode tirar proveito daqui, tornando esse código significativamente mais lento. Mesmo votado.
ShadowRanger 24/02
2

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]
Daniil Mashkin
fonte
-1

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)
Ace.Di
fonte