Como acelerar a execução de uma função em Python (#python #cache #dev)

Como acelerar a execução de uma função em Python

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:

  1. Criar duas versões da função: com cache e sem cache;
  2. Usar timeit para medir o desempenho de cada uma.
  3. 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. 🙂

Traduções: