Curso
Iteradores são objetos que podem ser percorridos. Eles são um recurso comum da linguagem Python, muito usados em loops e compreensões de lista. Qualquer objeto do qual se possa obter um iterador é chamado de iterável.
Criar um iterador dá trabalho. Por exemplo, a implementação de cada objeto iterador deve incluir os métodos __iter__() e __next__() . Além disso, a implementação precisa de uma forma de acompanhar o estado interno do objeto e lançar a exceção StopIteration quando não houver mais valores a retornar. Essas regras formam o protocolo do iterador.
Implementar seu próprio iterador é um processo demorado e nem sempre necessário. Uma alternativa mais simples é usar um generator. Generators são um tipo especial de função que usam a palavra-chave yield para retornar um iterador que pode ser percorrido, um valor por vez.
Saber identificar quando faz sentido implementar um iterador ou usar um generator vai aprimorar suas habilidades como programador Python. No restante deste tutorial, vamos destacar as diferenças entre esses dois objetos, ajudando você a escolher a melhor opção para cada situação.
Glossário
|
Termo |
Definição |
|
Iterável |
Um objeto Python que pode ser percorrido em um loop. Exemplos de iteráveis incluem listas, conjuntos, tuplas, dicionários, strings etc. |
|
Iterador |
Um objeto que pode ser percorrido. Portanto, iteradores contêm um número contável de valores. |
|
Generator |
Um tipo especial de função que não retorna um único valor: ela retorna um objeto iterador com uma sequência de valores. |
|
Avaliação preguiçosa |
Uma estratégia de avaliação em que certos objetos só são produzidos quando necessários. Por isso, alguns desenvolvedores também se referem a essa técnica como “call-by-need”. |
|
Protocolo do iterador |
Conjunto de regras que devem ser seguidas para definir um iterador em Python. |
|
next() |
Função built-in usada para retornar o próximo item de um iterador. |
|
iter() |
Função built-in usada para converter um iterável em um iterador. |
|
yield() |
Palavra-chave de Python semelhante a return, exceto que |
Iteradores e iteráveis em Python
Iteráveis são objetos capazes de retornar seus membros um de cada vez – isto é, podem ser percorridos. Estruturas de dados populares do Python, como listas, tuplas e conjuntos, são iteráveis. Outras estruturas como strings e dicionários também são consideradas iteráveis: uma string pode ser percorrida caractere a caractere, e é possível iterar sobre as chaves de um dicionário. Como regra geral, considere iterável qualquer objeto que possa ser usado em um for.
Explorando iteráveis em Python com exemplos
Dadas as definições, podemos concluir que todo iterador também é iterável. Porém, nem todo iterável é um iterador. Um iterável só produz um iterador quando é percorrido.
Para demonstrar, vamos instanciar uma lista, que é um iterável, e produzir um iterador chamando a função built-in iter() nessa lista.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
Embora a lista por si só não seja um iterador, chamar a função iter() a converte em um iterador e retorna o objeto iterador.
Para mostrar que nem todo iterável é iterador, vamos instanciar a mesma lista e tentar chamar a função next(), usada para retornar o próximo item de um iterador.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
No código acima, ao tentar chamar next() na lista, foi gerado um TypeError – saiba mais sobre tratamento de exceções e erros em Python. Isso acontece simplesmente porque uma lista é um iterável, não um iterador.
Explorando iteradores em Python com exemplos
Assim, se o objetivo é percorrer uma lista manualmente, primeiro é preciso criar um objeto iterador. Só então podemos controlar a iteração pelos valores da lista.
# instanciar uma lista
list_instance = [1, 2, 3, 4]
# converter a lista em iterador
iterator = iter(list_instance)
# retornar itens um por vez
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
O Python cria automaticamente um objeto iterador sempre que você tenta fazer um loop sobre um objeto iterável.
# instanciar uma lista
list_instance = [1, 2, 3, 4]
# percorrer a lista
for item in list_instance:
print(item)
"""
1
2
3
4
"""
Quando a exceção StopIteration é capturada, o loop é encerrado.
Os valores obtidos de um iterador só podem ser recuperados da esquerda para a direita. Python não possui uma função previous() para permitir voltar em um iterador.
A natureza preguiçosa dos iteradores
É possível definir vários iteradores a partir do mesmo objeto iterável. Cada iterador mantém seu próprio estado de progresso. Assim, ao criar várias instâncias de iterador de um mesmo iterável, você pode chegar ao fim de uma instância enquanto outra permanece no início.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Perceba que iterator_b imprime o primeiro elemento da sequência.
Podemos dizer, então, que iteradores têm natureza preguiçosa: quando um iterador é criado, os elementos não são gerados até serem solicitados. Em outras palavras, os elementos da nossa lista só seriam retornados quando pedíssemos explicitamente com next(iter(list_instance)).
No entanto, é possível extrair todos os valores de um iterador de uma vez chamando um contêiner iterável built-in (ou seja, list(), set(), tuple()) sobre o objeto iterador, forçando-o a gerar todos os elementos de uma vez.
# instanciar o iterável
list_instance = [1, 2, 3, 4]
# produzir um iterador a partir do iterável
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Isso não é recomendado para iteradores grandes, pois obriga todos os elementos a serem gerados e mantidos em memória de uma só vez, o que anula o benefício da avaliação preguiçosa.
Quando um conjunto de dados é grande demais para caber confortavelmente na memória, ou quando você quer iteração preguiçosa sem escrever uma classe de iterador completa, um generator costuma ser a melhor opção.
Generators em Python
A alternativa mais rápida a implementar um iterador é usar um generator. Embora generators se pareçam com funções Python comuns, eles são diferentes. Para começar, um generator não retorna itens diretamente. Em vez disso, usa a palavra-chave yield para gerar itens sob demanda. Assim, podemos dizer que um generator é um tipo especial de função que faz uso de avaliação preguiçosa.
Generators não armazenam seu conteúdo em memória como um iterável típico faria. Por exemplo, se a ideia fosse encontrar todos os divisores de um inteiro positivo, normalmente implementaríamos uma função tradicional (saiba mais sobre funções em Python neste tutorial) assim:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
O código acima retorna a lista completa de divisores. Agora repare a diferença quando usamos um generator em vez de uma função tradicional:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Como usamos yield em vez de return, a função não é encerrada após a execução. Na prática, pedimos ao Python para criar um objeto generator em vez de uma função tradicional, o que permite acompanhar o estado do generator.
Consequentemente, é possível chamar next() nesse iterador preguiçoso para exibir os elementos da sequência um por vez.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Outra forma de criar um generator é com uma compreensão de generator. A sintaxe é semelhante à de uma compreensão de lista, mas com parênteses em vez de colchetes.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Explorando a palavra-chave yield em Python
A palavra-chave yield controla o fluxo de uma função generator. Em vez de encerrar a função como acontece com return, yield retorna um valor e mantém o estado das variáveis locais.
O generator retornado pela chamada a yield pode ser atribuído a uma variável e percorrido com a função next() – isso executa a função até encontrar o primeiro yield. Ao chegar em yield, a execução da função é suspensa e seu estado é salvo. Assim, é possível retomar a execução quando quisermos.
Ao retomar, a função continua a partir do ponto do yield. Por exemplo:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
No código acima, nosso generator tem quatro chamadas a yield, mas tentamos chamar next cinco vezes, o que gerou uma exceção StopIteration. Isso acontece porque nosso generator não é uma série infinita; ao chamá-lo mais vezes do que o esperado, ele é esgotado.
Para fechar
Recapitulando: iteradores são objetos que podem ser percorridos, e generators são funções especiais que fazem avaliação preguiçosa. Ao implementar seu próprio iterador, você precisa criar os métodos __iter__() e __next__(); já um generator pode ser implementado usando a palavra-chave yield em uma função ou em uma compreensão em Python.
Você pode preferir um iterador personalizado em vez de um generator quando precisar de um objeto com lógica mais complexa de manutenção de estado ou quando quiser expor outros métodos além de __next__(), __iter__() e __init__(). Por outro lado, um generator pode ser mais adequado ao lidar com grandes volumes de dados, já que não armazena o conteúdo em memória, ou quando não é necessário implementar um iterador completo.

FAQS
Qual é a diferença entre um iterador e um generator em Python?
Um iterador é qualquer objeto que implementa __iter__() e __next__(). Um generator é uma forma mais simples de criar um iterador usando uma função com a palavra-chave yield. Todo generator é um iterador, mas nem todo iterador é um generator.
Quando devo usar um generator em vez de uma lista em Python?
Use um generator para sequências grandes ou infinitas, ou quando a eficiência de memória for importante. Listas mantêm todos os elementos na memória de uma vez, enquanto generators produzem um valor por vez. Para conjuntos pequenos que você vai reutilizar, uma lista costuma dar conta.
O que a palavra-chave yield faz em Python?
A palavra-chave yield transforma uma função em um generator. Em vez de retornar e encerrar, yield pausa a função, retorna um valor e memoriza seu estado para retomar na próxima chamada.
Como criar um generator em Python?
Você pode escrever uma função que use yield em vez de return, ou usar uma expressão de generator — a mesma sintaxe de uma compreensão de lista, mas com parênteses, como (x * 2 for x in range(10)).
Generators são mais rápidos que iteradores em Python?
Não em velocidade bruta, mas eles são mais eficientes em memória porque produzem valores sob demanda. Para grandes volumes de dados, isso costuma significar melhor desempenho geral; para conjuntos pequenos, a diferença é desprezível.