Pular para o conteúdo principal

Tutorial de iteradores e geradores Python

Explore a diferença entre Iteradores e Geradores do Python e saiba quais são os melhores para usar em várias situações.
24 de abr. de 2024  · 10 min de leitura

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. 

Temas

Principais cursos de Python

curso

Introduction to Functions in Python

3 hr
419.7K
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
Ver DetalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow
Relacionado

tutorial

Tutorial de indexação de lista Python()

Neste tutorial, você aprenderá exclusivamente sobre a função index().
Sejal Jaiswal's photo

Sejal Jaiswal

6 min

tutorial

Tutorial do For Loops em Python

Saiba como implementar For Loops em Python para iterar uma sequência ou as linhas e colunas de um dataframe do pandas.
Aditya Sharma's photo

Aditya Sharma

5 min

tutorial

Operadores em Python

Este tutorial aborda os diferentes tipos de operadores em Python, sobrecarga de operadores, precedência e associatividade.
Théo Vanderheyden's photo

Théo Vanderheyden

9 min

tutorial

Tutorial de lambda em Python

Aprenda uma maneira mais rápida de escrever funções em tempo real com as funções lambda.
DataCamp Team's photo

DataCamp Team

3 min

tutorial

Tutorial de conjuntos e teoria de conjuntos em Python

Aprenda sobre os conjuntos do Python: o que são, como criá-los, quando usá-los, funções incorporadas e sua relação com as operações da teoria dos conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

tutorial

Tutorial e exemplos de funções e métodos de lista do Python

Saiba mais sobre as funções e os métodos da Lista do Python. Siga exemplos de código para list() e outras funções e métodos Python agora!
Abid Ali Awan's photo

Abid Ali Awan

7 min

See MoreSee More