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:
# 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():
# 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 iterableQuando 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.
# 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 subscriptableDistinçã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.
35.1.4) Por Que a Iterabilidade Importa
Entender iterabilidade ajuda você a:
- Saber sobre o que você pode fazer loop: Qualquer iterável funciona com loops
for - Entender mensagens de erro: “object is not iterable” significa que você não pode usar isso em um loop
for - Usar compreensões: Compreensões de lista, conjunto e dicionário funcionam com qualquer iterável
- Trabalhar com funções embutidas: Muitas embutidas como
sum(),max(),min()esorted()aceitam qualquer iterável
# 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)
# 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:
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 StopIterationAqui está como isso fica explicitamente:
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 itemsO loop for lida com a exceção StopIteration automaticamente, e é por isso que você nunca a vê em código normal.
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:
# 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:
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: CharlieIsso é 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:
# 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: 3Ranges 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:
# 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 valores35.2.5) Iteradores de Dicionário
Dicionários fornecem diferentes iteradores para chaves, valores e itens:
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:
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:
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, 335.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:
# 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:
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 colors35.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:
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:
__iter__(): Retorna o próprio objeto iterador (geralmenteself)__next__(): Retorna o próximo valor na sequência ou levantaStopIterationquando termina
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: 5Vamos detalhar o que acontece:
- O loop
forchamaiter(counter), o que chamacounter.__iter__()e recebe de volta o própriocounter - O loop chama repetidamente
next(counter), o que chamacounter.__next__() - Cada chamada a
__next__()retorna o próximo número e incrementacurrent - Quando
current > end,__next__()levantaStopIteration, e o loop para
35.4.3) Uso Manual de Iteradores Personalizados
Você também pode usar iteradores personalizados manualmente com iter() e next():
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 exhausted35.4.4) Iteradores São Esgotáveis (Revisitado)
Lembre-se de que iteradores só podem ser usados uma vez:
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 exhaustedPara iterar novamente, você precisa criar uma nova instância:
# 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:
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, 3Esse padrão separa responsabilidades:
CounterIterableé o iterável—ele sabe como criar iteradoresCounterIteratoré 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:
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 California35.4.7) Quando Usar Iteradores Personalizados
Crie iteradores personalizados quando:
- Você precisa de avaliação preguiçosa: Gerar valores sob demanda em vez de armazenar todos
- Você tem uma estrutura de dados personalizada: Torná-la iterável para que funcione com loops
for - Você precisa de lógica de iteração especial: Pular itens, transformar valores ou implementar avanços complexos
- 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.