Python & AI Tutorials Logo
Programação Python

35. Como a Iteração Funciona: Iteráveis e Iteradores

Ao longo deste livro, você vem usando loops for para iterar sobre listas, strings, dicionários e outras coleções. Você escreveu código como for item in my_list: inúmeras vezes. Mas o que realmente acontece por trás dos panos quando o Python executa um loop for? Como o Python sabe como avançar por diferentes tipos de coleções?

Neste capítulo, vamos explorar o protocolo de iteração (iteration protocol)—o mecanismo que faz os loops for funcionarem. Você vai aprender sobre iteráveis (iterables) (objetos sobre os quais você pode fazer loop) e iteradores (iterators) (objetos que fazem de fato o avanço pelos valores). Entender essa distinção vai aprofundar seu conhecimento de como o Python funciona e preparar você para trabalhar com geradores no Capítulo 36.

35.1) O Que Significa um Objeto Ser Iterável

35.1.1) O Conceito de Iterabilidade

Um iterável (iterable) é qualquer objeto Python que pode receber um loop for. Quando dizemos “fazer loop”, queremos dizer que o Python consegue obter itens do objeto um de cada vez, em sequência.

Você já trabalhou com muitos iteráveis:

python
# Listas são iteráveis
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    print(num)  # Output: 1, 2, 3, 4, 5 (on separate lines)
 
# Strings são iteráveis
text = "Python"
for char in text:
    print(char)  # Output: P, y, t, h, o, n (on separate lines)
 
# Dicionários são iteráveis (por padrão, sobre as chaves)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)

Todos esses objetos—listas, strings, dicionários, tuplas, conjuntos, ranges e arquivos—são iteráveis porque eles dão suporte ao protocolo de iteração (iteration protocol) do Python (um conjunto de regras que permite que o Python faça loop sobre eles).

35.1.2) O Que Torna um Objeto Iterável

Para um objeto ser iterável, ele precisa implementar um método especial chamado __iter__(). Esse método retorna um objeto iterador. Não se preocupe com os detalhes ainda—vamos explorar iteradores na próxima seção.

Você pode verificar se um objeto é iterável tentando obter um iterador dele usando a função embutida iter():

python
# Testando se objetos são iteráveis
numbers = [1, 2, 3]
iterator = iter(numbers)  # Funciona - listas são iteráveis
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hello"
iterator = iter(text)  # Funciona - strings são iteráveis
print(type(iterator))  # Output: <class 'str_iterator'>
 
# Tentando com um objeto não iterável
value = 42
try:
    iterator = iter(value)  # Falha - inteiros não são iteráveis
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'int' object is not iterable

Quando você chama iter() em um objeto iterável, o Python chama o método __iter__() do objeto e retorna um iterador. Se o objeto não tiver esse método, você recebe um TypeError.

35.1.3) Iteráveis vs Sequências

É importante entender que nem todos os iteráveis são sequências. Uma sequência é um tipo específico de iterável que dá suporte a indexação e tem uma ordem definida.

python
# Sequências suportam indexação
my_list = [10, 20, 30]
print(my_list[0])  # Output: 10
 
my_string = "Python"
print(my_string[2])  # Output: t
 
# Conjuntos são iteráveis, mas NÃO são sequências (sem indexação, sem ordem garantida)
my_set = {1, 2, 3}
for item in my_set:
    print(item)  # Funciona - conjuntos são iteráveis
 
# Mas indexação não funciona
try:
    print(my_set[0])  # Falha - conjuntos não suportam indexação
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'set' object is not subscriptable

Distinção-chave: Todas as sequências (listas, tuplas, strings, ranges) são iteráveis, mas nem todos os iteráveis são sequências. Conjuntos e dicionários são iteráveis, mas não são sequências porque não suportam indexação.

Objetos Python

Iteráveis

Não iteráveis

Sequências

Iteráveis que não são sequências

Listas, Tuplas, Strings, Ranges

Conjuntos, Dicionários, Arquivos

Números, None, Booleanos

35.1.4) Por Que a Iterabilidade Importa

Entender iterabilidade ajuda você a:

  1. Saber sobre o que você pode fazer loop: Qualquer iterável funciona com loops for
  2. Entender mensagens de erro: “object is not iterable” significa que você não pode usar isso em um loop for
  3. Usar compreensões: Compreensões de lista, conjunto e dicionário funcionam com qualquer iterável
  4. Trabalhar com funções embutidas: Muitas embutidas como sum(), max(), min() e sorted() aceitam qualquer iterável
python
# Tudo isso funciona porque aceitam iteráveis
numbers = [1, 2, 3, 4, 5]
print(sum(numbers))  # Output: 15
 
text = "Python"
print(max(text))  # Output: y (highest alphabetically)
 
# Funciona até com conjuntos
unique_values = {10, 5, 20, 15}
print(sorted(unique_values))  # Output: [5, 10, 15, 20]

35.2) Iteradores do Dia a Dia em Python (Arquivos, Ranges, Dicionários e Mais)

35.2.1) O Que É um Iterador

Um iterador (iterator) é um objeto que representa um fluxo de dados. Ele retorna um valor por vez quando você pede o próximo item. Depois que um iterador retornou todos os seus valores, ele fica esgotado e não pode ser reutilizado.

Pense em um iterador como um marcador em um livro:

  • Ele lembra em que ponto você está na sequência
  • Você pode pedir o próximo item
  • Quando você chega ao fim, não dá para voltar sem criar um novo iterador

A principal diferença entre um iterável e um iterador:

  • Um iterável é algo sobre o qual você pode iterar (como uma lista)
  • Um iterador é o objeto que faz a iteração (o mecanismo que avança pela lista)
python
# Uma lista é um iterável
numbers = [1, 2, 3]
 
# Obtendo um iterador a partir do iterável
iterator = iter(numbers)
 
# O iterador é um objeto separado
print(type(numbers))    # Output: <class 'list'>
print(type(iterator))   # Output: <class 'list_iterator'>

35.2.2) Iteradores em Loops for

Quando você escreve um loop for, o Python cria automaticamente um iterador por trás dos panos:

python
numbers = [10, 20, 30]
 
# O que você escreve:
for num in numbers:
    print(num)
 
# O que o Python faz internamente (conceitualmente):
# 1. Call iter(numbers) to get an iterator
# 2. Repeatedly call next() on the iterator
# 3. Stop when the iterator raises StopIteration

Aqui está como isso fica explicitamente:

python
numbers = [10, 20, 30]
 
# Iteração manual (o que o for faz automaticamente)
iterator = iter(numbers)
try:
    print(next(iterator))  # Output: 10
    print(next(iterator))  # Output: 20
    print(next(iterator))  # Output: 30
    print(next(iterator))  # Would raise StopIteration
except StopIteration:
    print("No more items")  # Output: No more items

O loop for lida com a exceção StopIteration automaticamente, e é por isso que você nunca a vê em código normal.

Sim

Não -> StopIteration

for item in iterable:

Python calls iter(iterable)

Obtém um objeto iterador

Python calls next(iterator)

Mais itens?

Atribui a item

Executa o corpo do loop

Sai do loop

35.2.3) Objetos de Arquivo como Iteradores

Objetos de arquivo são excelentes exemplos de iteradores. Quando você itera sobre um arquivo, ele lê uma linha por vez:

python
# Crie um arquivo de exemplo
with open("students.txt", "w") as file:
    file.write("Alice\n")
    file.write("Bob\n")
    file.write("Charlie\n")
 
# Lendo o arquivo linha a linha
with open("students.txt", "r") as file:
    for line in file:
        print(line.strip())  # Output: Alice, Bob, Charlie (on separate lines)

Objetos de arquivo são ao mesmo tempo iteráveis e iteradores. Eles retornam a si mesmos quando você chama iter() neles:

python
with open("students.txt", "r") as file:
    iterator = iter(file)
    print(file is iterator)  # Output: True (same object)
    
    # Lendo linhas manualmente
    print(next(iterator))  # Output: Alice
    print(next(iterator))  # Output: Bob
    print(next(iterator))  # Output: Charlie

Isso é eficiente em termos de memória porque o Python não carrega o arquivo inteiro na memória—ele lê uma linha por vez conforme você pede.

35.2.4) Objetos Range como Iteradores

Objetos range são iteráveis que geram números sob demanda:

python
# Um range é um iterável
numbers = range(1, 4)
print(type(numbers))  # Output: <class 'range'>
 
# Obtendo um iterador a partir do range
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'range_iterator'>
 
# Usando o iterador
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3

Ranges são eficientes em termos de memória porque não armazenam todos os números na memória—eles calculam cada número quando solicitado:

python
# Este range representa 1 milhão de números, mas usa memória mínima
large_range = range(1000000)
print(type(large_range))  # Output: <class 'range'>
 
# Obtendo um iterador
iterator = iter(large_range)
print(next(iterator))  # Output: 0
print(next(iterator))  # Output: 1
# ... pode continuar por 1 milhão de valores

35.2.5) Iteradores de Dicionário

Dicionários fornecem diferentes iteradores para chaves, valores e itens:

python
student = {"name": "Alice", "age": 20, "grade": "A"}
 
# Iterando sobre chaves (padrão)
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)
 
# Obtendo explicitamente um iterador de chaves
keys_iterator = iter(student.keys())
print(next(keys_iterator))  # Output: name
print(next(keys_iterator))  # Output: age
 
# Iterando sobre valores
values_iterator = iter(student.values())
print(next(values_iterator))  # Output: Alice
print(next(values_iterator))  # Output: 20
 
# Iterando sobre itens (pares chave-valor)
items_iterator = iter(student.items())
print(next(items_iterator))  # Output: ('name', 'Alice')
print(next(items_iterator))  # Output: ('age', 20)

35.2.6) Iteradores São Esgotáveis

Uma propriedade crucial dos iteradores é que eles só podem ser usados uma vez. Depois de esgotados, eles não reiniciam:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
# Primeira passagem pelo iterador
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3
 
# Agora o iterador está esgotado
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("Iterator exhausted")  # Output: Iterator exhausted
 
# Para iterar novamente, crie um novo iterador
iterator = iter(numbers)
print(next(iterator))  # Output: 1 (fresh start)

Isso é diferente do próprio iterável, que pode ser iterado várias vezes:

python
numbers = [1, 2, 3]
 
# Primeira iteração
for num in numbers:
    print(num)  # Output: 1, 2, 3
 
# Segunda iteração (funciona bem - cria um novo iterador)
for num in numbers:
    print(num)  # Output: 1, 2, 3

35.3) Usando iter() e next() para Avançar por Iteráveis

35.3.1) A Função iter()

A função iter() recebe um iterável e retorna um iterador. Este é o primeiro passo do protocolo de iteração:

python
# Criando iteradores a partir de diferentes iteráveis
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator))  # Output: <class 'str_iterator'>
 
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator))  # Output: <class 'set_iterator'>

Cada tipo de iterável retorna seu próprio tipo especializado de iterador, mas todos funcionam do mesmo jeito—você chama next() para obter o próximo valor.

35.3.2) A Função next()

A função next() recupera o próximo item de um iterador. Quando não há mais itens, ela levanta StopIteration:

python
colors = ["red", "green", "blue"]
iterator = iter(colors)
 
# Obtendo itens um por vez
print(next(iterator))  # Output: red
print(next(iterator))  # Output: green
print(next(iterator))  # Output: blue
 
# Não há mais itens
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("No more colors")  # Output: No more colors

35.3.3) Fornecendo um Valor Padrão para next()

Você pode fornecer um valor padrão para next() como segundo argumento. Quando o iterador está esgotado, em vez de levantar uma exceção StopIteration, next() vai retornar o valor padrão que você especificou:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
print(next(iterator))           # Output: 1
print(next(iterator))           # Output: 2
print(next(iterator))           # Output: 3
print(next(iterator, "Done"))   # Output: Done (default value, no exception)
print(next(iterator, "Done"))   # Output: Done (still exhausted)

Isso é útil quando você quer lidar com o fim da iteração de forma elegante, sem tratamento de exceção:

35.4) Criando Iteradores Personalizados com iter e next

35.4.1) Por Que Criar Iteradores Personalizados

Os iteráveis embutidos do Python (listas, strings, arquivos) cobrem a maioria dos casos comuns. Porém, às vezes você precisa criar seus próprios objetos iteráveis para um comportamento especializado:

  • Gerar sequências com lógica personalizada
  • Iterar sobre estruturas de dados que você projeta
  • Criar iteração eficiente em memória sobre grandes conjuntos de dados
  • Implementar avaliação preguiçosa (lazy evaluation) (calcular valores só quando necessário)

Criar um iterador personalizado requer implementar dois métodos especiais: __iter__() e __next__().

35.4.2) O Protocolo do Iterador

Para tornar um objeto um iterador, ele precisa implementar:

  1. __iter__(): Retorna o próprio objeto iterador (geralmente self)
  2. __next__(): Retorna o próximo valor na sequência ou levanta StopIteration quando termina
python
class SimpleCounter:
    """An iterator that counts from start to end."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        """Return the iterator object (self)."""
        return self
    
    def __next__(self):
        """Return the next value or raise StopIteration."""
        if self.current > self.end:
            raise StopIteration
        
        value = self.current
        self.current += 1
        return value
 
# Using the custom iterator
counter = SimpleCounter(1, 5)
 
for num in counter:
    print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5

Vamos detalhar o que acontece:

  1. O loop for chama iter(counter), o que chama counter.__iter__() e recebe de volta o próprio counter
  2. O loop chama repetidamente next(counter), o que chama counter.__next__()
  3. Cada chamada a __next__() retorna o próximo número e incrementa current
  4. Quando current > end, __next__() levanta StopIteration, e o loop para

35.4.3) Uso Manual de Iteradores Personalizados

Você também pode usar iteradores personalizados manualmente com iter() e next():

python
counter = SimpleCounter(10, 13)
 
# Obtenha o iterador (retorna a si mesmo)
iterator = iter(counter)
print(iterator is counter)  # Output: True
 
# Obtenha valores manualmente
print(next(iterator))  # Output: 10
print(next(iterator))  # Output: 11
print(next(iterator))  # Output: 12
print(next(iterator))  # Output: 13
 
# Agora esgotado
try:
    print(next(iterator))
except StopIteration:
    print("Counter exhausted")  # Output: Counter exhausted

35.4.4) Iteradores São Esgotáveis (Revisitado)

Lembre-se de que iteradores só podem ser usados uma vez:

python
counter = SimpleCounter(1, 3)
 
# Primeira iteração
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# Segunda iteração (não funciona - o iterador está esgotado)
for num in counter:
    print(num)  # Nothing printed - iterator is already exhausted

Para iterar novamente, você precisa criar uma nova instância:

python
# Crie um novo contador para cada iteração
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3
 
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3 (new iterator)

35.4.5) Criando uma Classe Iterável (Não Apenas um Iterador)

Muitas vezes, você quer uma classe que seja iterável, mas que crie um iterador novo a cada vez. Para fazer isso, separe o iterável do iterador:

python
class CounterIterable:
    """An iterable that creates fresh counter iterators."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        """Return a new iterator each time."""
        return CounterIterator(self.start, self.end)
 
class CounterIterator:
    """The actual iterator that does the counting."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value
 
# Now we can iterate multiple times
counter = CounterIterable(1, 3)
 
# First iteration
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# Second iteration (works because __iter__ creates a new iterator)
for num in counter:
    print(num)  # Output: 1, 2, 3

Esse padrão separa responsabilidades:

  • CounterIterable é o iterável—ele sabe como criar iteradores
  • CounterIterator é o iterador—ele sabe como avançar pelos valores

35.4.6) Exemplo Prático: Iterando sobre uma Estrutura de Dados Personalizada

Vamos criar um iterador para uma estrutura de dados personalizada—uma playlist simples:

python
class Playlist:
    """A music playlist that can be iterated over."""
    
    def __init__(self):
        self.songs = []
    
    def add_song(self, title, artist):
        """Add a song to the playlist."""
        self.songs.append({"title": title, "artist": artist})
    
    def __iter__(self):
        """Return an iterator for the playlist."""
        return PlaylistIterator(self.songs)
 
class PlaylistIterator:
    """Iterator for stepping through songs in a playlist."""
    
    def __init__(self, songs):
        self.songs = songs
        self.index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= len(self.songs):
            raise StopIteration
        
        song = self.songs[self.index]
        self.index += 1
        return song
 
# Using the playlist
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
 
# Iterate over songs
print("Now playing:")
for song in playlist:
    print(f"  {song['title']} by {song['artist']}")
# Output: Now playing:
# Output:   Imagine by John Lennon
# Output:   Bohemian Rhapsody by Queen
# Output:   Hotel California by Eagles
 
# Can iterate again (creates a new iterator)
print("\nReplay:")
for song in playlist:
    print(f"  {song['title']}")
# Output: Replay:
# Output:   Imagine
# Output:   Bohemian Rhapsody
# Output:   Hotel California

35.4.7) Quando Usar Iteradores Personalizados

Crie iteradores personalizados quando:

  1. Você precisa de avaliação preguiçosa: Gerar valores sob demanda em vez de armazenar todos
  2. Você tem uma estrutura de dados personalizada: Torná-la iterável para que funcione com loops for
  3. Você precisa de lógica de iteração especial: Pular itens, transformar valores ou implementar avanços complexos
  4. Eficiência de memória importa: Gerar sequências grandes sem armazená-las

Porém, no Capítulo 36, você vai aprender sobre geradores, que fornecem uma forma muito mais simples de criar iteradores usando a palavra-chave yield. Geradores geralmente são preferidos em vez de implementar manualmente __iter__() e __next__() porque são mais concisos e mais fáceis de entender.

Entender como criar iteradores personalizados dá a você uma visão de como o protocolo de iteração do Python funciona, mesmo que você muitas vezes use geradores no lugar. Os conceitos que você aprendeu aqui—__iter__(), __next__() e StopIteration—são fundamentais para entender geradores e outras técnicas avançadas de iteração no próximo capítulo.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai