Pular para o conteúdo principal
InicioTutoriaisPython

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.
abr. de 2024  · 10 min leer

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

Course

Python Data Science Toolbox (Part 1)

3 hr
407.8K
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
See DetailsRight Arrow
Start Course
Veja MaisRight Arrow
Relacionado

blog

Como aprender Python do zero em 2024: um guia especializado

Descubra como aprender Python, suas aplicações e a demanda por competências em Python. Comece sua jornada em Python hoje mesmo ​com nosso guia detalhado.
Matt Crabtree's photo

Matt Crabtree

19 min

blog

Mais de 60 projetos Python para todos os níveis de conhecimento

60 ideias de projetos de ciência de dados que os cientistas de dados podem usar para criar um portfólio sólido, independentemente de sua especialização.
Bekhruz Tuychiev's photo

Bekhruz Tuychiev

16 min

tutorial

Como comentar um bloco de código em Python

O uso de comentários é fundamental para trabalhar efetivamente com Python. Neste breve tutorial, aprenda a comentar um bloco de código em Python.
Adel Nehme's photo

Adel Nehme

3 min

tutorial

Como aparar uma cadeia de caracteres em Python: Três métodos diferentes

Aprenda os fundamentos do corte de caracteres à esquerda e à direita de uma string em Python.
Adel Nehme's photo

Adel Nehme

5 min

tutorial

Declaração de caso de troca do Python: Um guia para iniciantes

Explore o match-case do Python: um guia sobre sua sintaxe, aplicativos em ciência de dados, ML e uma análise comparativa com o switch-case tradicional.
Matt Crabtree's photo

Matt Crabtree

5 min

tutorial

Tutorial de Python

Em Python, tudo é objeto. Números, cadeias de caracteres (strings), DataFrames, e até mesmo funções são objetos. Especificamente, qualquer coisa que você usa no Python tem uma classe, um modelo associado por trás.
DataCamp Team's photo

DataCamp Team

3 min

See MoreSee More