19/08/2024

Cache de função no Python

Utilizar um "cache de função" significa armazenar os resultados das chamadas de uma função, evitando reprocessamentos demorados para os mesmos argumentos.

A ideia é armazenar os argumentos (parâmetros) recebidos pela função e o resultado do processamento retornado pela função.

Assim, quando a função recebe novamente os mesmos argumentos, em vez de executar seu processamento para encontrar o resultado, apenas retorna o resultado previamente armazenado.

Vamos a um exemplo, para melhorar o entendimento.

Digamos que temos uma função que calcula a multiplicação de um número inteiro pelo seu sucessor:

def vezes_proximo(numero):
    print('número =', numero)
    proximo = numero + 1
    print('próximo =', proximo)
    resultado = numero * proximo
    print('resultado =', resultado)
    return resultado

Ao executarmos essa função pela primeira vez, imprimindo seu retorno:

print(vezes_proximo(2))

Obteremos as seguintes informações:

número = 2
próximo = 3
resultado = 6
6

E sempre que executarmos novamente exatamente o mesmo comando, obteremos o mesmo resultado, como esperado!

print(vezes_proximo(2))
número = 2
próximo = 3
resultado = 6
6

Porém, como evitar esse reprocessamento quando não desejado?

É justamente para isso que utilizamos um cache de função.

Implementação

Vou mostrar uma implementação de cache de função bem simples, com intuito meramente didático.

Podemos criar uma variável e uma função para nos ajudar a atingir o objetivo.

  • Uma variável, do tipo dicionário, para armazenar os resultados já calculados;
  • Uma função que chama a função que queremos fazer cache, no caso a "vezes_proximo", apenas quando necessário.

resultados_anteriores = {}

def cached_vezes_proximo(numero):
    if numero not in resultados_anteriores:
        resultados_anteriores[numero] = vezes_proximo(numero)
    return resultados_anteriores[numero]

O que acontece ao ser chamada a função "cached_vezes_proximo":

  • A função verifica se a argumento "numero" já está guardado como chave do dicionário "resultados_anteriores";
    • Se não está guardado, chama a função "vezes_proximo" e guarda o resultado no dicionário "resultados_anteriores";
  • Finalmente, retorna o resultado guardado em "resultados_anteriores", independentemente de ter sido calculado na hora ou anteriormente.

Assim, ao executarmos:

print(cached_vezes_proximo(2))

Obteremos as seguintes informações no terminal:

número = 2
próximo = 3
resultado = 6
6

Porém, ao executarmos novamente, obteremos apenas a seguinte informação no terminal:

6

Ou seja, na segunda execução não houve chamada à função "vezes_proximo". Apenas ocorreu o retorno do resultado que já tinha sido calculado na primeira chamada.

Quando utilizar

O cache de função é muito útil para economizar tempo de execução e poupar recursos computacionais custosos. Deve ser utilizado quando uma função:

  • Retorna sempre o mesmo resultado quando chamada com os mesmo argumentos;
  • Tem execução custosa, seja por processar muito internamente ou por utilizar recursos externos demorados;
  • É chamada muitas vezes com o mesmo argumento em um espaço de tempo relativamente curto.

Exemplo simples:

  • Sistema de Sugestões de Produtos Baseadas em Recomendações
    • Cenário: Um sistema de vendas com uma função que recebe o código do cliente e sugere produtos com base em algoritmos de recomendação.
    • Uso de Cache: Como os cálculos de recomendação podem ser complexos, os resultados podem ser armazenados em cache por um tempo para cada cliente, proporcionando sugestões rápidas sempre que o cliente recarregar uma página do sistema.

Neste exemplo, você pode ter percebido um detalhe não abordado no código: "resultados podem ser armazenados... por um tempo". Esse "detalhe" é muito importante. Falei em armazenar, mas armazenar quanto e por quanto tempo? Isso é uma discussão longa que poderemos tratar em outro momento.

Quando não utilizar

O cache de função não deve ser utilizado quando uma função:

  • Não retorna sempre o mesmo valor para os mesmos argumentos;
  • Não é chamada muitas vezes com os mesmos argumentos;
  • É muito simples, sendo mais leve reexecutar o cálculo do que controlar o cache.

Na prática

A solução de cache desenvolvida como exemplo neste artigo é trabalhosa e tem poucos recursos. Não cuida, por exemplo, do tamanho do cache de armazenamento e do ciclo de vida do dado armazenado.

Existem várias formas de implementar um cache em uma função no Python. Porém, abordarei apenas uma das mais comuns: utilizar o decorador "lru_cache", do módulo "functools", que está disponível por padrão no Python.

Em vez de criarmos uma variável e uma função auxiliares, podemos simplesmente importar e utilizar o "lru_cache", como demonstrado nas linhas em negrito abaixo:

from functools import lru_cache

@lru_cache(maxsize=128)

def vezes_proximo(numero):
    print('número =', 
numero)
    proximo = numero + 1
    print('próximo =', proximo)
    resultado = numero * proximo
   
print('resultado =', resultado)
    return resultado

Isso será o suficiente para termos:

print(vezes_proximo(2))
número = 2
próximo = 3
resultado = 6
6

print(vezes_proximo(2))
6

Mesmo sendo muito simples de implementar, essa solução é muito mais poderosa do que a desenvolvida inicialmente neste artigo, pois cuida do tamanho e do ciclo de vida dos dados armazenados no cache.

No caso, o argumento "maxsize" do decorador define o número máximo de resultados que o cache pode armazenar. Ao atingir a quantidade limite, o "lru_cache" descarta os resultados "menos recentemente usados" ("Least Recently Used"), daí o "lru_" no nome do decorador.

Links


Nenhum comentário:

Postar um comentário