Como acelerar a execução de uma função em Python (#python #cache #dev)
Overview
Este post mostra uma maneira de acelerar a execução de uma função indeppontente em Python. É uma maneira simples que pode facilmente passar batido.
Imagine o seguinte: Você escreveu um aplicativo Python incrível e ele está funcionando perfeitamente, mas leva um
tempo considerável para ser executado.
Após debuggar, você descobriu que é por causa de uma função cara que é chamada várias vezes. É uma função bem otimizada, então não há muito o que fazer para melhorar o desempenho, no entanto, essa função é indepente, o que significa que ela sempre retornará a mesma saída para a mesma entrada.
Existe uma solução de uma linha que pode ajudar a acelerar as coisas: @functools.cache
Vamos ver um exemplo usando a sequência de Fibonacci, já que calcular números desta sequência é uma operação computacionalmente cara.
n_map = {
0: 0,
1: 1,
2: 1,
}
def fibonacci(n):
if n <= 0:
return 0
return n_map.get(n, fibonacci(n - 1) + fibonacci(n - 2))
No exemplo acima, a função é chamada várias vezes (recursivamente) com a mesma entrada, e esperamos o mesmo resultado, então podemos usar esse decorator para armazenar em cache os resultados.
Neste teste, nós vamos:
- Criar duas versões da função: com cache e sem cache;
- Usar
timeit
para medir o desempenho de cada uma. - Criar um wrapper para capturar o resultado, para garantir que melhoramos o desempenho, mas não avacalhamos os resultados.
Esta é a versão com cache:
import functools
cached_result = -1
n_map = {
0: 0,
1: 1,
2: 1,
}
@functools.cache
def cached_fibonacci(n):
if n <= 0:
return 0
return n_map.get(n, cached_fibonacci(n - 1) + cached_fibonacci(n - 2))
e esta é a versão sem cache:
uncached_result = -1
n_map = {
0: 0,
1: 1,
2: 1,
}
def uncached_fibonacci(n):
if n <= 0:
return 0
return n_map.get(n, uncached_fibonacci(n - 1) + uncached_fibonacci(n - 2))
def uncached_result_wrapper(n):
global uncached_result
uncached_result = uncached_fibonacci(n)
(Por favor, ignore a duplicação de código, é para facilitar as coisas para este exemplo)
Por útlmo, este é o código para medir o desempenho:
import timeit
n = 40 # Desired Fibonacci number to test
cached_time_taken = timeit.timeit('cached_result_wrapper(n)', globals=globals(), number=1)
uncached_time_taken = timeit.timeit('uncached_result_wrapper(n)', globals=globals(), number=1)
print(f"Time taken to calculate the {n}th Fibonacci ({cached_result}) 1 times cached: {cached_time_taken:.6f} seconds")
print(f"Time taken to calculate the {n}th Fibonacci ({uncached_result}) 1 times uncached: {uncached_time_taken:.6f} seconds")
Executando isso (na minha máquina) o resultado foi o seguinte:
Time taken to calculate the 40th Fibonacci (102334155) 1 times cached: 0.000073 seconds
Time taken to calculate the 40th Fibonacci (102334155) 1 times uncached: 45.812993 seconds
Process finished with exit code 0
Sem alterar nada no código, apenas adicionando o @functools.cache
, o tempo de execução caiu de quase 46 segundos para
menos de 1 milissegundo.
O @functools.cache
funciona criando um wrapper na função e memoizando os resultados, ou seja, uma vez que um determinado
resultado foi calculado, ele é armazenado em cache e reutilizado sempre que a mesma entrada for fornecida.
Este decorator está disponível desde o Python 3.9, então certifique-se de estar usando uma versão compatível.
Se você estiver um pouco atrasado e não puder usá-lo, você pode usar o functools.lru_cache
, que está disponível a partir do Python 3.2.
- A documentação do
@functools.cache
pode ser encontrada aqui. - A documentação do
functools.lru_cache
pode ser encontrada aqui.
Se você quiser este exemplo funcionando, você pode pegá-lo no meu GithHub
Hope that helps. 🙂