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.
1n_map = {
2 0: 0,
3 1: 1,
4 2: 1,
5}
6
7def fibonacci(n):
8 if n <= 0:
9 return 0
10
11 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
timeitpara 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:
1import functools
2
3cached_result = -1
4
5n_map = {
6 0: 0,
7 1: 1,
8 2: 1,
9}
10
11@functools.cache
12def cached_fibonacci(n):
13 if n <= 0:
14 return 0
15
16 return n_map.get(n, cached_fibonacci(n - 1) + cached_fibonacci(n - 2))
e esta é a versão sem cache:
1uncached_result = -1
2
3n_map = {
4 0: 0,
5 1: 1,
6 2: 1,
7}
8
9def uncached_fibonacci(n):
10 if n <= 0:
11 return 0
12
13 return n_map.get(n, uncached_fibonacci(n - 1) + uncached_fibonacci(n - 2))
14
15def uncached_result_wrapper(n):
16 global uncached_result
17 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:
1import timeit
2
3n = 40 # Desired Fibonacci number to test
4cached_time_taken = timeit.timeit('cached_result_wrapper(n)', globals=globals(), number=1)
5uncached_time_taken = timeit.timeit('uncached_result_wrapper(n)', globals=globals(), number=1)
6
7print(f"Time taken to calculate the {n}th Fibonacci ({cached_result}) 1 times cached: {cached_time_taken:.6f} seconds")
8print(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:
1Time taken to calculate the 40th Fibonacci (102334155) 1 times cached: 0.000073 seconds
2Time taken to calculate the 40th Fibonacci (102334155) 1 times uncached: 45.812993 seconds
3
4Process 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.cachepode ser encontrada aqui. - A documentação do
functools.lru_cachepode ser encontrada aqui.
Se você quiser este exemplo funcionando, você pode pegá-lo no meu GithHub
Hope that helps. 🙂