Como reduzir o uso de memória e acelerar o código Python usando geradores

Olá pessoal. Hoje, queremos compartilhar uma tradução útil, preparada antes do lançamento do curso "Desenvolvedor Web em Python" . Escrever código com eficiência de tempo e memória em Python é especialmente importante ao criar um aplicativo da Web, modelo de aprendizado de máquina ou teste.



Quando comecei a aprender geradores em Python, não fazia ideia do quanto eles eram importantes. No entanto, eles sempre me ajudaram a escrever funções ao longo de minha jornada através do aprendizado de máquina.


As funções do gerador permitem declarar uma função que se comportará como um iterador. Eles permitem que os programadores criem iteradores rápidos, simples e limpos. Um iterador é um objeto que pode ser repetido (em loop). É usado para abstrair o contêiner de dados e fazê-lo se comportar como um objeto iterável. Por exemplo, um exemplo de um objeto iterável pode ser cadeias, listas e dicionários.


O gerador parece uma função, mas usa a palavra-chave yield em vez de return. Vejamos um exemplo para torná-lo mais claro.


def generate_numbers(): n = 0 while n < 3: yield n n += 1 

Esta é uma função de gerador. Quando você o chama, ele retorna um objeto gerador.


 >>> numbers = generate_numbers() >>> type(numbers) <class 'generator'> 

É importante prestar atenção em como o estado está encapsulado no corpo da função do gerador. Você pode iterar um de cada vez usando a função next () integrada:


 >>> next_number = generate_numbers() >>> next(next_number) 0 >>> next(next_number) 1 >>> next(next_number) 2 

O que acontece se você chamar next () após o final da execução?


StopIteration é um tipo interno de exceção que ocorre automaticamente assim que o gerador para de retornar um resultado. Este é um sinal de parada para o loop for.


Declaração de rendimento


Sua principal tarefa é controlar o fluxo da função do gerador para que pareça uma declaração de retorno. Quando uma função de gerador é chamada ou uma expressão de gerador é usada, ela retorna um iterador especial chamado gerador. Para usar um gerador, atribua-o a alguma variável. Ao chamar métodos especiais no gerador, como next (), o código da função será executado até o rendimento.


Quando entra na declaração de rendimento, o programa pausa a função e retorna o valor ao objeto que iniciou a execução. (Enquanto return interrompe a execução da função completamente.) Quando a função é suspensa, seu estado é preservado.


Agora que estamos familiarizados com geradores em Python, vamos comparar a abordagem usual com a abordagem que usa geradores em termos de memória e tempo gasto na execução de código.


Declaração do problema


Suponha que precisamos percorrer uma grande lista de números (por exemplo, 1.000.000.000) e salvar os quadrados de todos os números que precisam ser armazenados separadamente em outra lista.


Abordagem habitual


 import memory_profiler import time def check_even(numbers): even = [] for num in numbers: if num % 2 == 0: even.append(num*num) return even if __name__ == '__main__': m1 = memory_profiler.memory_usage() t1 = time.clock() cubes = check_even(range(100000000)) t2 = time.clock() m2 = memory_profiler.memory_usage() time_diff = t2 - t1 mem_diff = m2[0] - m1[0] print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method") 

Depois de executar o código acima, obtemos o seguinte:


 It took 21.876470000000005 Secs and 1929.703125 Mb to execute this method 

Usando geradores


 import memory_profiler import time def check_even(numbers): for num in numbers: if num % 2 == 0: yield num * num if __name__ == '__main__': m1 = memory_profiler.memory_usage() t1 = time.clock() cubes = check_even(range(100000000)) t2 = time.clock() m2 = memory_profiler.memory_usage() time_diff = t2 - t1 mem_diff = m2[0] - m1[0] print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method") 

Depois de executar o código acima, obtemos o seguinte:


 It took 2.9999999995311555e-05 Secs and 0.02656277 Mb to execute this method 

Como podemos ver, o tempo de execução e a memória usados ​​são significativamente reduzidos. Os geradores operam segundo um princípio conhecido como "computação lenta". Isso significa que eles podem economizar processador, memória e outros recursos de computação.


Conclusão


Espero que neste artigo eu tenha sido capaz de mostrar como os geradores em Python podem ser usados ​​para economizar recursos, como memória e tempo. Essa vantagem aparece devido ao fato de que os geradores não armazenam todos os resultados na memória, mas os calculam em tempo real, e a memória é usada apenas se solicitarmos o resultado dos cálculos. Os geradores também permitem que você abstraia uma grande quantidade de código padrão necessário para escrever iteradores, para que eles também ajudem a reduzir a quantidade de código.

Source: https://habr.com/ru/post/pt477926/


All Articles