Geradores Python
Geradores Python
Neste tutorial, você aprenderá como criar iterações facilmente usando geradores Python, como ele é diferente de iteradores e funções normais e por que você deve usá-lo.
Vídeo:Geradores Python
Geradores em Python
Há muito trabalho na construção de um iterador em Python. Temos que implementar uma classe com
__iter__()
e __next__()
método, acompanhe os estados internos e aumente StopIteration
quando não há valores a serem retornados. Isso é longo e contra-intuitivo. Generator vem em socorro em tais situações.
Geradores Python são uma maneira simples de criar iteradores. Todo o trabalho que mencionamos acima é tratado automaticamente por geradores em Python.
Simplesmente falando, um gerador é uma função que retorna um objeto (iterador) sobre o qual podemos iterar (um valor de cada vez).
Criar geradores em Python
É bastante simples criar um gerador em Python. É tão fácil quanto definir uma função normal, mas com um
yield
instrução em vez de um return
declaração. Se uma função contém pelo menos um
yield
declaração (pode conter outros yield
ou return
instruções), torna-se uma função geradora. Ambos yield
e return
retornará algum valor de uma função. A diferença é que enquanto um
return
termina uma função inteiramente, yield
A instrução pausa a função salvando todos os seus estados e depois continua a partir daí em sucessivas chamadas. Diferenças entre a função Gerador e a função Normal
Aqui está como uma função geradora difere de uma função normal.
- A função do gerador contém um ou mais
yield
declarações. - Quando chamado, retorna um objeto (iterador), mas não inicia a execução imediatamente.
- Métodos como
__iter__()
e__next__()
são implementados automaticamente. Assim, podemos percorrer os itens usandonext()
. - Depois que a função rende, a função é pausada e o controle é transferido para o chamador.
- Variáveis locais e seus estados são lembrados entre chamadas sucessivas.
- Finalmente, quando a função termina,
StopIteration
é gerado automaticamente em outras chamadas.
Aqui está um exemplo para ilustrar todos os pontos mencionados acima. Temos uma função geradora chamada
my_gen()
com vários yield
declarações.
# A simple generator function
def my_gen():
n = 1
print('This is printed first')
# Generator function contains yield statements
yield n
n += 1
print('This is printed second')
yield n
n += 1
print('This is printed at last')
yield n
Uma execução interativa no interpretador é fornecida abaixo. Execute-os no shell do Python para ver a saída.
>>> # It returns an object but does not start execution immediately.
>>> a = my_gen()
>>> # We can iterate through the items using next().
>>> next(a)
This is printed first
1
>>> # Once the function yields, the function is paused and the control is transferred to the caller.
>>> # Local variables and theirs states are remembered between successive calls.
>>> next(a)
This is printed second
2
>>> next(a)
This is printed at last
3
>>> # Finally, when the function terminates, StopIteration is raised automatically on further calls.
>>> next(a)
Traceback (most recent call last):
...
StopIteration
>>> next(a)
Traceback (most recent call last):
...
StopIteration
Uma coisa interessante a ser observada no exemplo acima é que o valor da variável n é lembrado entre cada chamada.
Ao contrário das funções normais, as variáveis locais não são destruídas quando a função rende. Além disso, o objeto gerador pode ser iterado apenas uma vez.
Para reiniciar o processo, precisamos criar outro objeto gerador usando algo como
a = my_gen()
. Uma última coisa a notar é que podemos usar geradores com loops for diretamente.
Isso ocorre porque um
for
loop pega um iterador e itera sobre ele usando next()
função. Ele termina automaticamente quando StopIteration
é levantada. Confira aqui para saber como um loop for é realmente implementado em Python.
# A simple generator function
def my_gen():
n = 1
print('This is printed first')
# Generator function contains yield statements
yield n
n += 1
print('This is printed second')
yield n
n += 1
print('This is printed at last')
yield n
# Using for loop
for item in my_gen():
print(item)
Ao executar o programa, a saída será:
This is printed first 1 This is printed second 2 This is printed at last 3
Geradores Python com um Loop
O exemplo acima é menos útil e nós o estudamos apenas para ter uma ideia do que estava acontecendo em segundo plano.
Normalmente, as funções do gerador são implementadas com um loop com uma condição de terminação adequada.
Vamos dar um exemplo de um gerador que inverte uma string.
def rev_str(my_str):
length = len(my_str)
for i in range(length - 1, -1, -1):
yield my_str[i]
# For loop to reverse the string
for char in rev_str("hello"):
print(char)
Saída
o l l e h
Neste exemplo, usamos o
range()
função para obter o índice na ordem inversa usando o loop for. Observação :Esta função geradora não só funciona com strings, mas também com outros tipos de iteráveis como lista, tupla, etc.
Expressão do Gerador Python
Geradores simples podem ser facilmente criados em tempo real usando expressões de gerador. Facilita a construção de geradores.
Semelhante às funções lambda que criam funções anônimas, expressões geradoras criam funções geradoras anônimas.
A sintaxe para expressão do gerador é semelhante à de uma compreensão de lista em Python. Mas os colchetes são substituídos por parênteses redondos.
A principal diferença entre uma compreensão de lista e uma expressão geradora é que uma compreensão de lista produz a lista inteira enquanto a expressão geradora produz um item por vez.
Eles têm execução preguiçosa (produzindo itens apenas quando solicitados). Por esse motivo, uma expressão geradora é muito mais eficiente em termos de memória do que uma compreensão de lista equivalente.
# Initialize the list
my_list = [1, 3, 6, 10]
# square each term using list comprehension
list_ = [x**2 for x in my_list]
# same thing can be done using a generator expression
# generator expressions are surrounded by parenthesis ()
generator = (x**2 for x in my_list)
print(list_)
print(generator)
Saída
[1, 9, 36, 100] <generator object <genexpr> at 0x7f5d4eb4bf50>
Podemos ver acima que a expressão geradora não produziu o resultado desejado imediatamente. Em vez disso, ele retornou um objeto gerador, que produz itens apenas sob demanda.
Aqui está como podemos começar a obter itens do gerador:
# Initialize the list
my_list = [1, 3, 6, 10]
a = (x**2 for x in my_list)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
next(a)
Quando executamos o programa acima, obtemos a seguinte saída:
1 9 36 100 Traceback (most recent call last): File "<string>", line 15, in <module> StopIteration
As expressões do gerador podem ser usadas como argumentos de função. Quando usado dessa maneira, os parênteses redondos podem ser eliminados.
>>> sum(x**2 for x in my_list)
146
>>> max(x**2 for x in my_list)
100
Uso de geradores Python
Existem várias razões que tornam os geradores uma implementação poderosa.
1. Fácil de implementar
Os geradores podem ser implementados de maneira clara e concisa em comparação com sua contraparte da classe iteradora. A seguir está um exemplo para implementar uma sequência de potência de 2 usando uma classe iteradora.
class PowTwo:
def __init__(self, max=0):
self.n = 0
self.max = max
def __iter__(self):
return self
def __next__(self):
if self.n > self.max:
raise StopIteration
result = 2 ** self.n
self.n += 1
return result
O programa acima era longo e confuso. Agora, vamos fazer o mesmo usando uma função de gerador.
def PowTwoGen(max=0):
n = 0
while n < max:
yield 2 ** n
n += 1
Como os geradores acompanham os detalhes automaticamente, a implementação foi concisa e muito mais limpa.
2. Memória eficiente
Uma função normal para retornar uma sequência criará a sequência inteira na memória antes de retornar o resultado. Isso é um exagero, se o número de itens na sequência for muito grande.
A implementação do gerador de tais sequências é amigável à memória e é preferida, pois produz apenas um item por vez.
3. Representar Fluxo Infinito
Os geradores são excelentes meios para representar um fluxo infinito de dados. Fluxos infinitos não podem ser armazenados na memória e, como os geradores produzem apenas um item por vez, eles podem representar um fluxo infinito de dados.
A seguinte função geradora pode gerar todos os números pares (pelo menos em teoria).
def all_even():
n = 0
while True:
yield n
n += 2
4. Geradores de pipeline
Vários geradores podem ser usados para canalizar uma série de operações. Isso é melhor ilustrado usando um exemplo.
Suponha que tenhamos um gerador que produz os números da série de Fibonacci. E temos outro gerador para elevar os números ao quadrado.
Se quisermos descobrir a soma dos quadrados dos números na série de Fibonacci, podemos fazê-lo da seguinte maneira, canalizando a saída das funções geradoras.
def fibonacci_numbers(nums):
x, y = 0, 1
for _ in range(nums):
x, y = y, x+y
yield x
def square(nums):
for num in nums:
yield num**2
print(sum(square(fibonacci_numbers(10))))
Saída
4895
Este pipelining é eficiente e fácil de ler (e sim, muito mais legal!).
python