Course
As funções Python nem sempre têm uma declaração return
. As funções geradoras são funções que têm a palavra-chave yield
em vez de return
.
Essas funções produzem iteradores de geradores, que são objetos que representam um fluxo de dados. Os elementos representados por um iterador são criados e cedidos somente quando necessário. Esse tipo de avaliação é geralmente chamado de avaliação preguiçosa.
Ao lidar com grandes conjuntos de dados, os geradores oferecem uma alternativa eficiente em termos de memória para o armazenamento de dados em listas, tuplas e outras estruturas de dados que exigem espaço na memória para cada um de seus elementos. As funções geradoras também podem criar iteradores infinitos, o que não é possível com estruturas avaliadas com ansiedade, como listas e tuplas.
Antes de começarmos, vamos recapitular as diferenças entre funções e geradores:
Recurso |
Função |
Gerador |
Produção de valor |
Retorna todos os valores de uma só vez |
Produz valores um de cada vez, sob demanda |
Execução |
Executa completamente antes de retornar |
Faz uma pausa depois de ceder, retoma quando o próximo valor é solicitado |
Palavra-chave |
retorno |
rendimento |
Uso da memória |
Potencialmente alto, armazena toda a sequência na memória |
Baixo, armazena apenas o valor e o estado atuais para o próximo |
Iteração |
Várias iterações são possíveis, mas requerem o armazenamento de toda a sequência |
Projetado para iteração de passagem única, mais eficiente para sequências grandes ou infinitas |
Usando a função Python yield
para criar funções de gerador
O termo gerador em Python pode se referir a um iterador gerador ou a uma função geradora. Esses são objetos diferentes, mas relacionados em Python. Neste tutorial, os termos completos são usados com frequência para evitar confusão.
Primeiro, vamos explorar as funções do gerador. Uma função geradora é semelhante a uma função normal, mas contém a palavra-chave yield
em vez de return
.
Quando um programa Python chama uma função geradora, ele cria um iterador gerador. Os iteradores produzem um valor sob demanda e pausam sua execução até que outro valor seja necessário. Vamos dar uma olhada em um exemplo para explicar esse conceito e demonstrar a diferença entre funções regulares e funções geradoras.
Usando uma função regular
Primeiro, vamos definir uma função regular, que contém uma instrução return
. Essa função aceita uma sequência de palavras e uma letra e retorna uma lista contendo o número de ocorrências da letra em cada palavra:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
A função gera uma lista contendo 1, 3 e 0, pois há um a em apple, três ocorrências de a em banana e nenhuma em cherry. A mesma função pode ser refatorada para usar compreensões de lista em vez de inicializar uma lista vazia e usar .append()
:
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
Essa função regular retorna uma lista contendo todos os resultados sempre que é chamada. No entanto, se a lista de palavras for grande, chamar essa função regular exige muito da memória, pois o programa cria e armazena uma nova lista do mesmo tamanho que a original. Se essa função for usada repetidamente em vários argumentos de entrada, ou se funções semelhantes estiverem realizando outras operações nos dados originais, a pressão na memória poderá aumentar rapidamente.
Usando uma função de gerador
Em vez disso, você pode usar uma função geradora:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
A função inclui a palavra-chaveyield
em vez de return
. Essa função de gerador retorna um objeto gerador quando chamada, que é atribuído a output
. Esse objeto é um iterador. Ele não contém os dados que representam o número de ocorrências da letra em cada palavra. Em vez disso, o gerador criará e produzirá os valores quando necessário. Vamos buscar o primeiro valor desse iterador gerador:
print(next(output))
1
A função integrada next()
é uma maneira de obter o próximo valor de um iterador. Veremos outras maneiras mais adiante neste tutorial.
O código na função geradora é executado até que o programa atinja a linha com a palavra-chave yield
. Neste exemplo, o loop for
inicia sua primeira iteração e busca o primeiro elemento da lista words
. O método de cadeia de caracteres .count()
retorna um número inteiro, que, nesse caso, é 1, pois há uma ocorrência de a em apple. O gerador produz esse valor, que é retornado por next(output)
.
O gerador output
interrompe sua execução nesse ponto. Portanto, o gerador concluiu a primeira iteração do loop for
e encontrou o número de ocorrências da letra a na primeira palavra da lista de palavras. Agora, ele está aguardando até que seja necessário novamente.
Se o next()
incorporado for chamado novamente com output
como argumento, o gerador retomará a execução a partir do ponto em que foi pausado:
print(next(output))
3
O gerador continua a partir da linha com yield
na primeira iteração do loop for
. Como o loop for
não tem mais linhas de código, ele retorna ao topo do loop e busca o segundo elemento da lista words
. O valor retornado por .count()
é 3 nesse caso, e esse valor é gerado. O gerador faz uma nova pausa nesse ponto da execução.
A terceira chamada para next()
retoma essa execução:
print(next(output))
0
A segunda iteração chega ao final do loop for
, que passa para a terceira iteração. O código avança para a linha com yield
novamente, desta vez produzindo o inteiro 0, pois não há nenhuma ocorrência de a em cherry.
O gerador faz uma nova pausa. O programa só determina o destino do gerador quando chamamos next()
uma quarta vez:
print(next(output))
Traceback (most recent call last):
...
StopIteration
A execução é retomada a partir do final da terceira iteração do loop for
. No entanto, o loop chegou ao fim de sua iteração, pois não há mais elementos na lista words
. O gerador gera uma exceção StopIteration
.
Na maioria dos casos de uso, os elementos do gerador não são acessados diretamente usando next()
, mas por meio de outro processo de iteração. A exceção StopIteration
sinaliza o fim do processo de iteração. Exploraremos isso mais detalhadamente na próxima seção deste tutorial.
O Python tem outra maneira de criar iteradores geradores quando sua operação pode ser representada por uma única expressão, como no exemplo anterior. O iterador do gerador output
pode ser criado usando uma _expressão do gerador_:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
A expressão entre parênteses atribuída a output
é uma expressão geradora, que cria um iterador gerador semelhante ao produzido pela função geradora find_letter_occurrences()
.
Vamos concluir esta seção com outro exemplo de uma função geradora para destacar como a execução é pausada e retomada sempre que um elemento é necessário:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
Essa função geradora não tem um loop. Em vez disso, ele contém três linhas que têm a palavra-chave yield
. O código cria um iterador gerador status
quando você chama a função geradora show_status()
. Na primeira vez que o programa chama next(status)
, o gerador inicia a execução. Ele imprime a string "Start"
e faz uma pausa após a primeira expressão yield
. O gerador gera None
, pois não há nenhum objeto após a palavra-chave yield
.
O programa imprime a cadeia de caracteres "Middle"
somente quando next()
é chamado uma segunda vez:
next(status)
Middle
O gerador faz uma pausa após a segunda expressão yield
. A terceira chamada para next()
imprime a string final, "End"
:
next(status)
End
O gerador faz uma pausa na expressão final yield
. Você abrirá uma exceção StopIteration
na próxima vez que o programa solicitar um valor desse iterador gerador:
next(status)
Traceback (most recent call last):
...
StopIteration
Exploraremos mais maneiras de usar geradores na seção a seguir.
Trabalhando com iteradores de gerador
As funções de gerador criam iteradores de gerador, e os iteradores são iteráveis. Vamos analisar essa frase. Toda vez que o programa chama uma função geradora, ele cria um iterador. Como os iteradores são iteráveis, eles podem ser usados em for
loops e outros processos iterativos.
Portanto, a função integrada next()
não é a única maneira de acessar elementos em um iterador. Esta seção explora outras formas de trabalhar com geradores.
Usando o protocolo de iteração do Python com iteradores de gerador
Vamos revisitar uma função geradora de uma seção anterior deste tutorial:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
Em vez de usar next()
várias vezes, essa versão do código usa o iterador do gerador output
em um loop for
. Como os iteradores são iteráveis, eles podem ser usados em for
loops. O loop busca itens no iterador do gerador até que não haja mais valores.
Ao contrário das estruturas de dados, como listas e tuplas, um iterador só pode ser usado uma vez. O código não imprime os valores novamente se tentarmos executar o mesmo loop for
uma segunda vez:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
O iterador é exaurido pelo primeiro loop for
, portanto, não pode mais gerar valores. Se o gerador for necessário novamente depois de esgotado, devemos criar outro iterador de gerador a partir da função de gerador.
Também é possível que você tenha vários iteradores de geradores em um programa ao mesmo tempo:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
A função geradora find_letter_occurrences()
cria dois iteradores de gerador: first_output
e second_output
. Embora ambos os iteradores se refiram aos mesmos dados na lista words
, eles progridem independentemente um do outro.
Este exemplo obtém o primeiro valor de first_output
usando next()
. O iterador do gerador produz 1 e faz uma pausa nesse ponto. O programa faz um loop em second_output
next. Como esse gerador ainda não produziu nenhum valor, o loop percorre todos os valores produzidos pelo segundo iterador. Por fim, há outro loop for
iterando por first_output
. No entanto, esse iterador já produziu seu primeiro valor anteriormente no programa. O loop percorre os valores restantes nesse iterador.
O loop for
não é o único processo que pode ser usado para iterar por meio de iteradores de geradores:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
Nesses exemplos, o programa chama a função do gerador diretamente para criar e usar o iterador do gerador em vez de atribuí-lo a uma variável. No primeiro exemplo, o iterador é descompactado usando a notação em estrela. Esse processo se baseia no mesmo protocolo de iteração que o loop for
.
No segundo exemplo, o iterador do gerador é passado para o built-in sorted()
, que requer um argumento iterável. Os geradores são iteráveis e, portanto, podem ser usados sempre que a iteração do Python ocorrer.
Criação de iteradores infinitos
Um gerador produz um valor e faz uma pausa até que o próximo valor seja necessário. Toda vez que o código solicitar um valor de um iterador, o código na função geradora será executado até que a próxima expressão yield
seja avaliada. Em todos os exemplos deste tutorial até agora, a função geradora tinha um número finito de expressões yield
. No entanto, é possível criar um gerador que produza um número infinito de valores usando um loop while
na função geradora. No exemplo a seguir, o gerador produz uma cor aleatória da lista de cores passada para a função geradora:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
A função geradora get_color()
tem uma expressão yield
em um loop while
. Portanto, o código sempre encontrará outra expressão yield
ao procurar o próximo valor. O iterador gerador output_colors
produz um número infinito de cores escolhidas aleatoriamente na lista de entrada. Esse gerador nunca se esgotará.
Não é possível criar estruturas de dados infinitas, como listas e tuplas. Os geradores permitem que um programa crie iteráveis infinitos. Observe que se o iterador do gerador for usado diretamente em um loop for
, o loop será executado para sempre.
Conceitos avançados de geradores
Os geradores têm casos de uso mais avançados em Python. Esta seção explorará alguns deles.
Envio de um objeto para o gerador
Os geradores também podem aceitar dados adicionais que podem ser usados durante a avaliação do código. A instrução que contém a palavra-chave yield
é uma expressão que é avaliada como um valor. Esse valor pode ser atribuído a uma variável dentro da função geradora. Vamos começar com um exemplo básico para demonstrar esse conceito:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
A palavra-chave Python yield
cria uma expressão que é avaliada como um valor. No entanto, o valor dessa expressão na função geradora não é o mesmo objeto gerado pelo gerador. Considere a primeira expressão yield
. O gerador produz o número inteiro 1. Portanto, o site print(next(output))
exibe 1 na primeira vez em que é chamado e pausa a execução do gerador.
No entanto, a expressão yield
no gerador é avaliada como um objeto, que o código atribui ao nome da variável value
. Neste exemplo, yield
atribui None
a value
. Esse processo é repetido para a segunda ocorrência de yield
na função geradora. O objetivo da terceira chamada next()
é garantir que todo o código na função geradora seja executado.
Vamos substituir a segunda e a terceira chamadas para next()
por .send()
que é um método na classe do gerador:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
A função do gerador não foi alterada. O gerador é iniciado chamando next()
, e o código é executado até produzir o primeiro número inteiro, 1. Em vez de usar next()
na segunda vez, o programa chama output.send()
. Esse método envia um objeto para o gerador. Neste exemplo, o objeto é uma string. A expressão yield
na função geradora é avaliada como essa cadeia de caracteres, que é atribuída a value
. Portanto, o gerador pode usar a string em seu código.
A segunda chamada para .send()
envia um novo objeto para o gerador, que é atribuído à mesma variável value
. O gerador gera um StopIteration
após a última chamada print()
, pois não há mais expressões yield
.
Vamos dar uma olhada em outro exemplo usando .send()
. O gerador a seguir exibe o saldo em uma conta, mas o saldo pode ser atualizado:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
A função de gerador requer um saldo inicial quando chamada. O valor de balance
pode mudar à medida que o gerador executa o código. Qualquer objeto enviado ao gerador usando .send()
é atribuído a amount
. Essa variável será None
se o gerador gerar um valor sem nenhum objeto enviado a ele, ou conterá o objeto enviado usando .send()
.
O iterador do gerador current_balance
começa com um saldo de US$ 100. O gerador é iniciado chamando next()
, que começa a executar o código até que o primeiro valor seja gerado.
Depois que o gerador for iniciado, você poderá reiniciar a execução usando .send()
em vez de next()
. O gerador adiciona o valor enviado ao saldo. Se nenhum valor for enviado, por exemplo, se você chamar next()
novamente, o gerador produzirá o saldo inalterado.
Obtendo diretamente de outro iterável
Os geradores Python também podem gerar valores diretamente de outro gerador ou iterável usando a sintaxe yield from
. Vejamos um exemplo de uma função geradora que produz valores de uma lista aninhada:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
A função geradora aceita uma lista, que pode incluir listas aninhadas dentro dela. O loop for
percorre os itens da lista. Cada item da lista externa é um valor, nesse caso, um número inteiro, ou outra lista. Quando o item não é uma lista, o gerador produz o item.
No entanto, quando o item é uma lista, o gerador chama recursivamente a função geradora flatten()
novamente com a lista interna como argumento. Isso cria outro iterador gerador, que usa a lista interna como sua fonte de dados. Se essa linha usasse uma expressão yield
, o primeiro gerador produziria o segundo gerador. Em vez disso, ao usar yield from
, o primeiro gerador produz valores do segundo gerador.
Resumo: yield
vs. return
As definições de função com return
e yield
são semelhantes, mas seu comportamento é diferente. Vamos resumir as principais diferenças:
Função regular |
Função do gerador |
|
Palavra-chave |
return (implícito se não for usado explicitamente) |
rendimento |
Chamado |
Executa o código até que o retorno seja alcançado e, em seguida, retorna o valor final |
Cria um iterador de gerador |
Rescisão |
Encerrado pelo comando return |
Pausado por rendimento, pode ser retomado mais tarde |
Valor de retorno |
Objeto único (pode ser uma estrutura de dados) |
Gerador de iterador |
Expressão de rendimento |
Não aplicável (cria uma declaração) |
É avaliado como None ou como o valor enviado usando .send() |
Casos de uso |
Ideal para retornar um resultado final |
Ideal para criar um fluxo de dados, especialmente sequências grandes ou infinitas |
Conclusão
A palavra-chave yield
do Python é usada em funções para definir uma função geradora. Quando chamadas, essas funções criam iteradores de geradores. Os geradores são um exemplo de avaliação preguiçosa em Python, em que as expressões são avaliadas quando o valor é necessário, em vez de quando você executa a expressão. Portanto, a expressão yield
é útil para criar um fluxo de dados em que os valores são gerados sob demanda, sem a necessidade de armazená-los na memória.
As considerações de eficiência são importantes ao lidar com grandes conjuntos de dados que exigem muitas operações. Os iteradores geradores do Python são uma das principais ferramentas necessárias para que você manipule grandes quantidades de dados com eficiência.
Se você quiser saber mais sobre Python, confira este Programa de carreira para desenvolvedores Python.
Aprenda Python com estes cursos!
Course
Intermediate Python for Developers
Track
Associate Python Developer
tutorial
Tutorial de iteradores e geradores Python
tutorial
Tutorial de compreensão de dicionário Python
tutorial
Tutorial de funções Python
tutorial
Função Python Print()
tutorial
Tutorial de lambda em Python
DataCamp Team
3 min
tutorial