curso
Tutorial de iteradores e geradores Python
Iteradores são objetos que podem ser iterados. Eles servem como um recurso comum da linguagem de programação Python, bem escondidos para looping e compreensões de lista. Qualquer objeto que possa derivar um iterador é conhecido como iterável.
Há muito trabalho envolvido na construção de um iterador. Por exemplo, a implementação de cada objeto iterador deve consistir em um método __iter__()
e __next__()
. Além do pré-requisito acima, a implementação também deve ter uma maneira de rastrear o estado interno do objeto e gerar uma exceção StopIteration
quando não for possível retornar mais valores. Essas regras são conhecidas como protocolo do iterador.
Implementar seu próprio iterador é um processo demorado e só às vezes é necessário. Uma alternativa mais simples é usar um objeto gerador. Os geradores são um tipo especial de função que usa a palavra-chave yield
para retornar um iterador que pode ser iterado, um valor de cada vez.
A capacidade de discernir os cenários apropriados para implementar um iterador ou usar um gerador aprimorará suas habilidades como programador Python. No restante deste tutorial, enfatizaremos as distinções entre os dois objetos, o que o ajudará a decidir qual é o melhor objeto a ser usado em várias situações.
Glossário
Prazo |
Definição |
Iterável |
Um objeto Python que pode ser submetido a um loop ou iterado em um loop. Exemplos de iteráveis incluem listas, conjuntos, tuplas, dicionários, strings etc. |
Iterador |
Um iterador é um objeto que pode ser iterado. Assim, os iteradores contêm um número contável de valores. |
Gerador |
Um tipo especial de função que não retorna um único valor: retorna um objeto iterador com uma sequência de valores. |
Avaliação preguiçosa |
Uma estratégia de avaliação em que determinados objetos são produzidos somente quando necessário. Consequentemente, alguns círculos de desenvolvedores também se referem à avaliação preguiçosa como "call-by-need". |
Protocolo de Iterador |
Um conjunto de regras que devem ser seguidas para definir um iterador em Python. |
next() |
Uma função integrada usada para retornar o próximo item em um iterador. |
iter() |
Uma função interna usada para converter um iterável em um iterador. |
yield() |
Uma palavra-chave python semelhante à palavra-chave return, exceto que yield retorna um objeto gerador em vez de um valor. |
Iteradores e iteráveis em Python
Iteráveis são objetos capazes de retornar seus membros um de cada vez - eles podem ser iterados. As estruturas de dados incorporadas populares do Python, como listas, tuplas e conjuntos, são qualificadas como iteráveis. Outras estruturas de dados, como strings e dicionários, também são consideradas iteráveis: uma string pode produzir iteração de seus caracteres, e as chaves de um dicionário podem ser iteradas. Como regra geral, considere qualquer objeto que possa ser iterado em um loop for como um iterável.
Explorando os iteráveis do Python com exemplos
Dadas as definições, podemos concluir que todos os iteradores também são iteráveis. No entanto, todo iterável não é necessariamente um iterador. Um iterável produz um iterador somente quando é iterado.
Para demonstrar essa funcionalidade, instanciaremos uma lista, que é um iterável, e produziremos um iterador chamando a função integrada iter()
na 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, a chamada da função iter()
a converte em um iterador e retorna o objeto iterador.
Para demonstrar que nem todos os iteráveis são iteradores, instanciaremos o mesmo objeto de lista e tentaremos chamar a função next()
, que é usada para retornar o próximo item em 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, você pode ver que a tentativa de chamar a função next()
na lista gerou uma TypeError
- saiba mais sobre Exceção e Tratamento de Erros em Python. Esse comportamento ocorreu pelo simples fato de que um objeto de lista é um iterável e não um iterador.
Explorando iteradores Python com exemplos
Portanto, se o objetivo for iterar em uma lista, um objeto iterador deverá ser produzido primeiro. Só então poderemos gerenciar a iteração dos valores da lista.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
O Python produz automaticamente um objeto iterador sempre que você tenta fazer um loop em um objeto iterável.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for iterator in list_instance:
print(iterator)
"""
1
2
3
4
"""
Quando a exceção StopIteration
é capturada, o loop termina.
Os valores obtidos de um iterador só podem ser recuperados da esquerda para a direita. O Python não tem uma função previous()
para permitir que os desenvolvedores retrocedam em um iterador.
A natureza preguiçosa dos iteradores
É possível definir vários iteradores com base no mesmo objeto iterável. Cada iterador manterá seu próprio estado de progresso. Assim, ao definir várias instâncias de iterador de um objeto iterável, é possível iterar até o final de uma instância enquanto a outra instância 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
"""
Observe que iterator_b
imprime o primeiro elemento da série.
Portanto, podemos dizer que os iteradores têm uma natureza preguiçosa: quando um iterador é criado, os elementos não são fornecidos até que sejam solicitados. Em outras palavras, os elementos de nossa instância de lista só seriam retornados quando solicitássemos explicitamente que fossem com next(iter(list_instance))
.
No entanto, todos os valores de um iterador podem ser extraídos de uma só vez chamando um contêiner de estrutura de dados iterável incorporado (ou seja, list()
, set()
, tuple()
) no objeto iterador para forçar o iterador a gerar todos os seus elementos de uma só vez.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Não é recomendável executar essa ação, especialmente quando os elementos retornados pelo iterador forem grandes, pois o processamento será demorado.
Sempre que um arquivo de dados grande sobrecarregar a memória do computador ou você tiver uma função que exija que seu estado interno seja mantido a cada chamada, mas a criação de um iterador não fizer sentido, dadas as circunstâncias, uma alternativa melhor é usar um objeto gerador.
Geradores Python
A alternativa mais conveniente para implementar um iterador é usar um gerador. Embora os geradores possam se parecer com funções comuns do Python, eles são diferentes. Para começar, um objeto gerador não retorna itens. Em vez disso, ele usa a palavra-chave yield
para gerar itens em tempo real. Assim, podemos dizer que um gerador é um tipo especial de função que aproveita a avaliação preguiçosa.
Os geradores não armazenam seu conteúdo na memória, como seria de se esperar de um iterável típico. Por exemplo, se o objetivo fosse encontrar todos os fatores de um número inteiro positivo, normalmente implementaríamos uma função tradicional (saiba mais sobre funções Python neste tutorial) da seguinte forma:
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 fatores. No entanto, observe a diferença quando um gerador é usado em vez de uma função tradicional do Python:
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 a palavra-chave yield
em vez de return
, a função não é encerrada após a execução. Em essência, dissemos ao Python para criar um objeto gerador em vez de uma função tradicional, o que permite que o estado do objeto gerador seja rastreado.
Consequentemente, é possível chamar a função next()
no iterador preguiçoso para mostrar os elementos da série um de cada 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 maneira de criar um gerador é com uma compreensão do gerador. As expressões geradoras adotam uma sintaxe semelhante à de uma compreensão de lista, exceto pelo fato de usarem colchetes arredondados em vez de colchetes quadrados.
print((val for val in range(1, 20+1) if n % val == 0))
"""
<generator object <genexpr> at 0x7fd940c31e50>
"""
Explorando os recursos do Python yield
Palavra-chave
A palavra-chave yield
controla o fluxo de uma função de gerador. Em vez de sair da função, como ocorre quando return
é usada, a palavra-chave yield
retorna a função, mas lembra o estado de suas variáveis locais.
O gerador retornado da chamada yield
pode ser atribuído a uma variável e iterado com a palavra-chave next()
- isso executará a função até a primeira palavra-chave yield
que encontrar. Quando a palavra-chave yield
é atingida, a execução da função é suspensa. Quando isso ocorre, o estado da função é salvo. Dessa forma, é possível retomar a execução da função conforme nossa própria vontade.
A função continuará a partir da chamada para yield
. Por exemplo:
def yield_multiple_statments():
yield "This is the first statment"
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_statments()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statment
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 gerador tem quatro chamadas yield
, mas tentamos chamar next cinco vezes, o que gerou uma exceção StopIteration
. Esse comportamento ocorreu porque nosso gerador não é uma série infinita, portanto, chamá-lo mais vezes do que o esperado esgotou o gerador.
Resumo
Para recapitular, os iteradores são objetos que podem ser iterados e os geradores são funções especiais que aproveitam a avaliação preguiçosa. Implementar seu próprio iterador significa que você deve criar um método __iter__()
e __next__()
, enquanto um gerador pode ser implementado usando a palavra-chave yield em uma função ou compreensão Python.
Talvez você prefira usar um iterador personalizado em vez de um gerador quando precisar de um objeto com comportamento complexo de manutenção de estado ou se quiser expor outros métodos além de __next__()
, __iter__()
e __init__()
. Por outro lado, um gerador pode ser preferível ao lidar com grandes conjuntos de dados, pois não armazena seu conteúdo na memória ou quando não é necessário implementar um iterador.
Principais cursos de Python
curso
Python Toolbox
curso
Intermediate Python
tutorial
Tutorial de indexação de lista Python()
tutorial
Tutorial do For Loops em Python
tutorial
Operadores em Python
tutorial
Tutorial de lambda em Python
DataCamp Team
3 min
tutorial
Tutorial de conjuntos e teoria de conjuntos em Python
DataCamp Team
13 min
tutorial