Rendimento Python, geradores e expressões de gerador

Neste tutorial, você aprenderá como criar iterações facilmente usando geradores Python, como é 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 o método __iter__()e __next__(), controlar os estados internos e aumentar StopIterationquando não houver valores a serem retornados.

Isso é demorado e contra-intuitivo. O Generator vem ao resgate em tais situações.

Os 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).

Crie geradores em Python

É bastante simples criar um gerador em Python. É tão fácil quanto definir uma função normal, mas com uma yielddeclaração em vez de uma returndeclaração.

Se uma função contém pelo menos uma yieldinstrução (pode conter outras instruções yieldou return), ela se torna uma função geradora. Ambos yielde returnretornarão algum valor de uma função.

A diferença é que, enquanto uma returninstrução termina uma função completamente, a yieldinstrução pausa a função salvando todos os seus estados e, posteriormente, continua a partir daí em chamadas sucessivas.

Diferenças entre a função do gerador e a função normal

Aqui está como uma função de gerador difere de uma função normal.

  • A função do gerador contém uma ou mais yieldinstruções.
  • Quando chamado, ele retorna um objeto (iterador), mas não inicia a execução imediatamente.
  • Métodos como __iter__()e __next__()são implementados automaticamente. Portanto, podemos iterar pelos itens usando next().
  • Depois que a função cede, ela é 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 declarados acima. Temos uma função geradora nomeada my_gen()com várias yieldinstruçõ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 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 observar 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 produz. Além disso, o objeto gerador pode ser iterado apenas uma vez.

Para reiniciar o processo, precisamos criar outro objeto gerador usando algo semelhante a = my_gen().

Uma última coisa a se notar é que podemos usar geradores com loops for diretamente.

Isso ocorre porque um forloop pega um iterador e itera sobre ele usando a next()função. Termina automaticamente quando StopIterationé gerado. Verifique 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)

Quando você executa o programa, a saída será:

 Isto é impresso primeiro 1 Isto é impresso em segundo 2 Isto é impresso no final 3

Geradores Python com um Loop

O exemplo acima é de menos utilidade 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)

Resultado

 Olleh

Neste exemplo, usamos a range()função para obter o índice na ordem inversa usando o loop for.

Observação : esta função geradora não funciona apenas 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, as expressões geradoras criam funções geradoras anônimas.

A sintaxe da expressão do gerador é semelhante à de uma compreensão de lista em Python. Mas os colchetes são substituídos por parênteses arredondados.

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.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

 # 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)

Output

 (1, 9, 36, 100) 

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

 # 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)

When we run the above program, we get the following output:

 1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

 >>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

 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

The above program was lengthy and confusing. Now, let's do the same using a generator function.

 def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

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 pipelining

Vários geradores podem ser usados ​​para canalizar uma série de operações. Isso é melhor ilustrado com um exemplo.

Suponha que temos 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 fazer isso da seguinte maneira, juntando 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))))

Resultado

 4895

Este pipelining é eficiente e fácil de ler (e sim, muito mais legal!).

Artigos interessantes...