Iteradores são objetos que podem ser percorridos. Eles são um recurso comum da linguagem Python, muito usados em loops e compreensões de lista. Qualquer objeto do qual se possa obter um iterador é chamado de iterável.
Criar um iterador dá trabalho. Por exemplo, a implementação de cada objeto iterador deve incluir os métodos __iter__() e __next__(). Além disso, a implementação precisa de uma forma de acompanhar o estado interno do objeto e lançar a exceção StopIteration quando não houver mais valores a retornar. Essas regras formam o protocolo do iterador.
Implementar seu próprio iterador é um processo demorado e nem sempre necessário. Uma alternativa mais simples é usar um generator. Generators são um tipo especial de função que usam a palavra-chave yield para retornar um iterador que pode ser percorrido, um valor por vez.
Saber identificar quando faz sentido implementar um iterador ou usar um generator vai aprimorar suas habilidades como programador Python. No restante deste tutorial, vamos destacar as diferenças entre esses dois objetos, ajudando você a escolher a melhor opção para cada situação.
Glossário
Termo
Definição
Iterável
Um objeto Python que pode ser percorrido em um loop. Exemplos de iteráveis incluem listas, conjuntos, tuplas, dicionários, strings etc.
Iterador
Um objeto que pode ser percorrido. Portanto, iteradores contêm um número contável de valores.
Generator
Um tipo especial de função que não retorna um único valor: ela retorna um objeto iterador com uma sequência de valores.
Avaliação preguiçosa
Uma estratégia de avaliação em que certos objetos só são produzidos quando necessários. Por isso, alguns desenvolvedores também se referem a essa técnica como “call-by-need”.
Protocolo do iterador
Conjunto de regras que devem ser seguidas para definir um iterador em Python.
next()
Função built-in usada para retornar o próximo item de um iterador.
iter()
Função built-in usada para converter um iterável em um iterador.
yield()
Palavra-chave de Python semelhante a return, exceto que yield retorna um objeto generator 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 – isto é, podem ser percorridos. Estruturas de dados populares do Python, como listas, tuplas e conjuntos, são iteráveis. Outras estruturas como strings e dicionários também são consideradas iteráveis: uma string pode ser percorrida caractere a caractere, e é possível iterar sobre as chaves de um dicionário. Como regra geral, considere iterável qualquer objeto que possa ser usado em um for.
Explorando iteráveis em Python com exemplos
Dadas as definições, podemos concluir que todo iterador também é iterável. Porém, nem todo iterável é um iterador. Um iterável só produz um iterador quando é percorrido.
Para demonstrar, vamos instanciar uma lista, que é um iterável, e produzir um iterador chamando a função built-in iter() nessa lista.
list_instance =[1,2,3,4]print(iter(list_instance))"""
<list_iterator object at 0x7fd946309e90>
"""Tecnologia
Embora a lista por si só não seja um iterador, chamar a função iter() a converte em um iterador e retorna o objeto iterador.
Para mostrar que nem todo iterável é iterador, vamos instanciar a mesma lista e tentar chamar a função next(), usada para retornar o próximo item de 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
"""Tecnologia
No código acima, ao tentar chamar next() na lista, foi gerado um TypeError – saiba mais sobre tratamento de exceções e erros em Python. Isso acontece simplesmente porque uma lista é um iterável, não um iterador.
Explorando iteradores em Python com exemplos
Assim, se o objetivo é percorrer uma lista manualmente, primeiro é preciso criar um objeto iterador. Só então podemos controlar a iteração pelos valores da lista.
# instanciar uma lista
list_instance =[1,2,3,4]# converter a lista em iterador
iterator =iter(list_instance)# retornar itens um por vezprint(next(iterator))print(next(iterator))print(next(iterator))print(next(iterator))"""
1
2
3
4
"""Tecnologia
O Python cria automaticamente um objeto iterador sempre que você tenta fazer um loop sobre um objeto iterável.
# instanciar uma lista
list_instance =[1,2,3,4]# percorrer a listafor item in list_instance:print(item)"""
1
2
3
4
"""Tecnologia
Quando a exceção StopIteration é capturada, o loop é encerrado.
Os valores obtidos de um iterador só podem ser recuperados da esquerda para a direita. Python não possui uma função previous() para permitir voltar em um iterador.
A natureza preguiçosa dos iteradores
É possível definir vários iteradores a partir do mesmo objeto iterável. Cada iterador mantém seu próprio estado de progresso. Assim, ao criar várias instâncias de iterador de um mesmo iterável, você pode chegar ao fim de uma instância enquanto outra permanece no início.
Perceba que iterator_b imprime o primeiro elemento da sequência.
Podemos dizer, então, que iteradores têm natureza preguiçosa: quando um iterador é criado, os elementos não são gerados até serem solicitados. Em outras palavras, os elementos da nossa lista só seriam retornados quando pedíssemos explicitamente com next(iter(list_instance)).
No entanto, é possível extrair todos os valores de um iterador de uma vez chamando um contêiner iterável built-in (ou seja, list(), set(), tuple()) sobre o objeto iterador, forçando-o a gerar todos os elementos de uma vez.
# instanciar o iterável
list_instance =[1,2,3,4]# produzir um iterador a partir do iterável
iterator =iter(list_instance)print(list(iterator))"""
[1, 2, 3, 4]
"""Tecnologia
Isso não é recomendado para iteradores grandes, pois obriga todos os elementos a serem gerados e mantidos em memória de uma só vez, o que anula o benefício da avaliação preguiçosa.
Quando um conjunto de dados é grande demais para caber confortavelmente na memória, ou quando você quer iteração preguiçosa sem escrever uma classe de iterador completa, um generator costuma ser a melhor opção.
Generators em Python
A alternativa mais rápida a implementar um iterador é usar um generator. Embora generators se pareçam com funções Python comuns, eles são diferentes. Para começar, um generator não retorna itens diretamente. Em vez disso, usa a palavra-chave yield para gerar itens sob demanda. Assim, podemos dizer que um generator é um tipo especial de função que faz uso de avaliação preguiçosa.
Generators não armazenam seu conteúdo em memória como um iterável típico faria. Por exemplo, se a ideia fosse encontrar todos os divisores de um inteiro positivo, normalmente implementaríamos uma função tradicional (saiba mais sobre funções em Python neste tutorial) assim:
deffactors(n):
factor_list =[]for val inrange(1, n+1):if n % val ==0:
factor_list.append(val)return factor_list
print(factors(20))"""
[1, 2, 4, 5, 10, 20]
"""Tecnologia
O código acima retorna a lista completa de divisores. Agora repare a diferença quando usamos um generator em vez de uma função tradicional:
deffactors(n):for val inrange(1, n+1):if n % val ==0:yield val
print(factors(20))"""
<generator object factors at 0x7fd938271350>
"""Tecnologia
Como usamos yield em vez de return, a função não é encerrada após a execução. Na prática, pedimos ao Python para criar um objeto generator em vez de uma função tradicional, o que permite acompanhar o estado do generator.
Consequentemente, é possível chamar next() nesse iterador preguiçoso para exibir os elementos da sequência um por vez.
deffactors(n):for val inrange(1, n+1):if n % val ==0:yield val
factors_of_20 = factors(20)print(next(factors_of_20))"""
1
"""Tecnologia
Outra forma de criar um generator é com uma compreensão de generator. A sintaxe é semelhante à de uma compreensão de lista, mas com parênteses em vez de colchetes.
factor_gen =(val for val inrange(1,21)if20% val ==0)print(list(factor_gen))"""
[1, 2, 4, 5, 10, 20]
"""Tecnologia
Explorando a palavra-chave yield em Python
A palavra-chave yield controla o fluxo de uma função generator. Em vez de encerrar a função como acontece com return, yield retorna um valor e mantém o estado das variáveis locais.
O generator retornado pela chamada a yield pode ser atribuído a uma variável e percorrido com a função next() – isso executa a função até encontrar o primeiro yield. Ao chegar em yield, a execução da função é suspensa e seu estado é salvo. Assim, é possível retomar a execução quando quisermos.
Ao retomar, a função continua a partir do ponto do yield. Por exemplo:
defyield_multiple_statements():yield"This is the first statement"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_statements()print(next(example))print(next(example))print(next(example))print(next(example))print(next(example))"""
This is the first statement
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:
"""Tecnologia
No código acima, nosso generator tem quatro chamadas a yield, mas tentamos chamar next cinco vezes, o que gerou uma exceção StopIteration. Isso acontece porque nosso generator não é uma série infinita; ao chamá-lo mais vezes do que o esperado, ele é esgotado.
Para fechar
Recapitulando: iteradores são objetos que podem ser percorridos, e generators são funções especiais que fazem avaliação preguiçosa. Ao implementar seu próprio iterador, você precisa criar os métodos __iter__() e __next__(); já um generator pode ser implementado usando a palavra-chave yield em uma função ou em uma compreensão em Python.
Você pode preferir um iterador personalizado em vez de um generator quando precisar de um objeto com lógica mais complexa de manutenção de estado ou quando quiser expor outros métodos além de __next__(), __iter__() e __init__(). Por outro lado, um generator pode ser mais adequado ao lidar com grandes volumes de dados, já que não armazena o conteúdo em memória, ou quando não é necessário implementar um iterador completo.
Qual é a diferença entre um iterador e um generator em Python?⌄
Um iterador é qualquer objeto que implementa __iter__() e __next__(). Um generator é uma forma mais simples de criar um iterador usando uma função com a palavra-chave yield. Todo generator é um iterador, mas nem todo iterador é um generator.
Quando devo usar um generator em vez de uma lista em Python?⌄
Use um generator para sequências grandes ou infinitas, ou quando a eficiência de memória for importante. Listas mantêm todos os elementos na memória de uma vez, enquanto generators produzem um valor por vez. Para conjuntos pequenos que você vai reutilizar, uma lista costuma dar conta.
O que a palavra-chave yield faz em Python?⌄
A palavra-chave yield transforma uma função em um generator. Em vez de retornar e encerrar, yield pausa a função, retorna um valor e memoriza seu estado para retomar na próxima chamada.
Como criar um generator em Python?⌄
Você pode escrever uma função que use yield em vez de return, ou usar uma expressão de generator — a mesma sintaxe de uma compreensão de lista, mas com parênteses, como (x * 2 for x in range(10)).
Generators são mais rápidos que iteradores em Python?
⌄
Não em velocidade bruta, mas eles são mais eficientes em memória porque produzem valores sob demanda. Para grandes volumes de dados, isso costuma significar melhor desempenho geral; para conjuntos pequenos, a diferença é desprezível.
Um tutorial introdutório completo sobre loops em Python. Aprenda e pratique loops while e for, loops aninhados, as palavras-chave break e continue, a função range e muito mais!
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.
Iteradores são objetos que podem ser percorridos. Eles são um recurso comum da linguagem Python, muito usados em loops e compreensões de lista. Qualquer objeto do qual se possa obter um iterador é chamado de iterável.
Criar um iterador dá trabalho. Por exemplo, a implementação de cada objeto iterador deve incluir os métodos
__iter__()e__next__(). Além disso, a implementação precisa de uma forma de acompanhar o estado interno do objeto e lançar a exceçãoStopIterationquando não houver mais valores a retornar. Essas regras formam o protocolo do iterador.Implementar seu próprio iterador é um processo demorado e nem sempre necessário. Uma alternativa mais simples é usar um generator. Generators são um tipo especial de função que usam a palavra-chave
yieldpara retornar um iterador que pode ser percorrido, um valor por vez.Saber identificar quando faz sentido implementar um iterador ou usar um generator vai aprimorar suas habilidades como programador Python. No restante deste tutorial, vamos destacar as diferenças entre esses dois objetos, ajudando você a escolher a melhor opção para cada situação.
Glossário
Termo
Definição
Iterável
Um objeto Python que pode ser percorrido em um loop. Exemplos de iteráveis incluem listas, conjuntos, tuplas, dicionários, strings etc.
Iterador
Um objeto que pode ser percorrido. Portanto, iteradores contêm um número contável de valores.
Generator
Um tipo especial de função que não retorna um único valor: ela retorna um objeto iterador com uma sequência de valores.
Avaliação preguiçosa
Uma estratégia de avaliação em que certos objetos só são produzidos quando necessários. Por isso, alguns desenvolvedores também se referem a essa técnica como “call-by-need”.
Protocolo do iterador
Conjunto de regras que devem ser seguidas para definir um iterador em Python.
next()
Função built-in usada para retornar o próximo item de um iterador.
iter()
Função built-in usada para converter um iterável em um iterador.
yield()
Palavra-chave de Python semelhante a return, exceto que
yieldretorna um objeto generator 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 – isto é, podem ser percorridos. Estruturas de dados populares do Python, como listas, tuplas e conjuntos, são iteráveis. Outras estruturas como strings e dicionários também são consideradas iteráveis: uma string pode ser percorrida caractere a caractere, e é possível iterar sobre as chaves de um dicionário. Como regra geral, considere iterável qualquer objeto que possa ser usado em um for.
Explorando iteráveis em Python com exemplos
Dadas as definições, podemos concluir que todo iterador também é iterável. Porém, nem todo iterável é um iterador. Um iterável só produz um iterador quando é percorrido.
Para demonstrar, vamos instanciar uma lista, que é um iterável, e produzir um iterador chamando a função built-in
iter()nessa lista.Embora a lista por si só não seja um iterador, chamar a função
iter()a converte em um iterador e retorna o objeto iterador.Para mostrar que nem todo iterável é iterador, vamos instanciar a mesma lista e tentar chamar a função
next(), usada para retornar o próximo item de um iterador.No código acima, ao tentar chamar
next()na lista, foi gerado umTypeError– saiba mais sobre tratamento de exceções e erros em Python. Isso acontece simplesmente porque uma lista é um iterável, não um iterador.Explorando iteradores em Python com exemplos
Assim, se o objetivo é percorrer uma lista manualmente, primeiro é preciso criar um objeto iterador. Só então podemos controlar a iteração pelos valores da lista.
O Python cria automaticamente um objeto iterador sempre que você tenta fazer um loop sobre um objeto iterável.
Quando a exceção
StopIterationé capturada, o loop é encerrado.Os valores obtidos de um iterador só podem ser recuperados da esquerda para a direita. Python não possui uma função
previous()para permitir voltar em um iterador.A natureza preguiçosa dos iteradores
É possível definir vários iteradores a partir do mesmo objeto iterável. Cada iterador mantém seu próprio estado de progresso. Assim, ao criar várias instâncias de iterador de um mesmo iterável, você pode chegar ao fim de uma instância enquanto outra permanece no início.
Perceba que
iterator_bimprime o primeiro elemento da sequência.Podemos dizer, então, que iteradores têm natureza preguiçosa: quando um iterador é criado, os elementos não são gerados até serem solicitados. Em outras palavras, os elementos da nossa lista só seriam retornados quando pedíssemos explicitamente com
next(iter(list_instance)).No entanto, é possível extrair todos os valores de um iterador de uma vez chamando um contêiner iterável built-in (ou seja,
list(),set(),tuple()) sobre o objeto iterador, forçando-o a gerar todos os elementos de uma vez.Isso não é recomendado para iteradores grandes, pois obriga todos os elementos a serem gerados e mantidos em memória de uma só vez, o que anula o benefício da avaliação preguiçosa.
Quando um conjunto de dados é grande demais para caber confortavelmente na memória, ou quando você quer iteração preguiçosa sem escrever uma classe de iterador completa, um generator costuma ser a melhor opção.
Generators em Python
A alternativa mais rápida a implementar um iterador é usar um generator. Embora generators se pareçam com funções Python comuns, eles são diferentes. Para começar, um generator não retorna itens diretamente. Em vez disso, usa a palavra-chave
yieldpara gerar itens sob demanda. Assim, podemos dizer que um generator é um tipo especial de função que faz uso de avaliação preguiçosa.Generators não armazenam seu conteúdo em memória como um iterável típico faria. Por exemplo, se a ideia fosse encontrar todos os divisores de um inteiro positivo, normalmente implementaríamos uma função tradicional (saiba mais sobre funções em Python neste tutorial) assim:
O código acima retorna a lista completa de divisores. Agora repare a diferença quando usamos um generator em vez de uma função tradicional:
Como usamos
yieldem vez dereturn, a função não é encerrada após a execução. Na prática, pedimos ao Python para criar um objeto generator em vez de uma função tradicional, o que permite acompanhar o estado do generator.Consequentemente, é possível chamar
next()nesse iterador preguiçoso para exibir os elementos da sequência um por vez.Outra forma de criar um generator é com uma compreensão de generator. A sintaxe é semelhante à de uma compreensão de lista, mas com parênteses em vez de colchetes.
Explorando a palavra-chave yield em Python
A palavra-chave
yieldcontrola o fluxo de uma função generator. Em vez de encerrar a função como acontece comreturn,yieldretorna um valor e mantém o estado das variáveis locais.O generator retornado pela chamada a
yieldpode ser atribuído a uma variável e percorrido com a funçãonext()– isso executa a função até encontrar o primeiroyield. Ao chegar emyield, a execução da função é suspensa e seu estado é salvo. Assim, é possível retomar a execução quando quisermos.Ao retomar, a função continua a partir do ponto do
yield. Por exemplo:No código acima, nosso generator tem quatro chamadas a
yield, mas tentamos chamar next cinco vezes, o que gerou uma exceçãoStopIteration. Isso acontece porque nosso generator não é uma série infinita; ao chamá-lo mais vezes do que o esperado, ele é esgotado.Para fechar
Recapitulando: iteradores são objetos que podem ser percorridos, e generators são funções especiais que fazem avaliação preguiçosa. Ao implementar seu próprio iterador, você precisa criar os métodos
__iter__()e__next__(); já um generator pode ser implementado usando a palavra-chave yield em uma função ou em uma compreensão em Python.Você pode preferir um iterador personalizado em vez de um generator quando precisar de um objeto com lógica mais complexa de manutenção de estado ou quando quiser expor outros métodos além de
__next__(),__iter__()e__init__(). Por outro lado, um generator pode ser mais adequado ao lidar com grandes volumes de dados, já que não armazena o conteúdo em memória, ou quando não é necessário implementar um iterador completo.