curso
Cache do Python: Dois métodos simples
Neste artigo, você aprenderá sobre cache em Python. Vamos entender o que é e como você pode usá-lo de forma eficaz.
O armazenamento em cache é uma técnica usada para melhorar o desempenho do aplicativo, armazenando temporariamente os resultados obtidos pelo programa para reutilizá-los se necessário posteriormente.
Neste tutorial, aprenderemos diferentes técnicas de armazenamento em cache no Python, incluindo os decoradores @lru_cache
e @cache
no módulo functools
.
Para você que está com pressa, vamos começar com uma implementação de cache muito curta e depois continuar com mais detalhes.
Resposta curta: Implementação de cache do Python
Para criar um cache em Python, você pode usar o decorador @cache
do módulo functools
. No código abaixo, observe que a função print()
é executada apenas uma vez:
import functools
@functools.cache
def square(n):
print(f"Calculating square of {n}")
return n * n
# Testing the cached function
print(square(4)) # Output: Calculating square of 4 \n 16
print(square(4)) # Output: 16 (cached result, no recalculation)
Calculating square of 4
16
16
O que é cache em Python?
Suponhamos que você precise resolver um problema matemático e gaste uma hora para obter a resposta correta. Se tivéssemos que resolver o mesmo problema no dia seguinte, seria útil reutilizar nosso trabalho anterior em vez de começar tudo de novo.
O armazenamento em cache no Python segue um princípio semelhante: ele armazena valores quando eles são calculados em chamadas de função para reutilizá-los quando necessário novamente. Esse tipo de armazenamento em cache também é chamado de memoização.
Vamos dar uma olhada em um pequeno exemplo que calcula a soma de uma grande variedade de números duas vezes:
output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)
5000000050000000
5000000050000000
O programa precisa calcular a soma todas as vezes. Podemos confirmar isso cronometrando as duas chamadas:
import timeit
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
1.2157779589979327
1.1848394999979064
O resultado mostra que ambas as chamadas levam aproximadamente o mesmo tempo (dependendo da nossa configuração, podemos obter tempos de execução mais rápidos ou mais lentos).
No entanto, podemos usar um cache para evitar que você calcule o mesmo valor mais de uma vez. Podemos redefinir o nome sum
usando a função cache()
no módulo functools
incorporado:
import functools
import timeit
sum = functools.cache(sum)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
1.2760689580027247
2.3330067051574588e-06
A segunda chamada agora leva alguns microssegundos em vez de mais de um segundo porque o resultado de encontrar a soma dos números de 0 a 100.000.000 já foi calculado e armazenado em cache - a segunda chamada usa o valor que foi calculado e armazenado anteriormente.
Acima, usamos o decorador functools.cache()
para incluir um cache na função interna sum()
. Como observação, um decorador em Python é uma função que modifica o comportamento de outra função sem alterar permanentemente seu código. Você pode saber mais sobre decoradores neste tutorial sobre decoradores do Python.
O decorador functools.cache()
foi adicionado ao Python na versão 3.9, mas você pode usar o functools.lru_cache()
para versões mais antigas. Na próxima seção, exploraremos essas duas maneiras de criar um cache, inclusive usando a notação de decorador mais usada, como @cache
.
Cache do Python: Métodos diferentes
O módulo functools
do Python tem dois decoradores para aplicar o cache a funções. Vamos explorar functools.lru_cache()
e functools.cache()
com um exemplo.
Vamos escrever uma função sum_digits()
que recebe uma sequência de números e retorna a soma dos dígitos desses números. Por exemplo, se você usar a tupla (23, 43, 8)
como entrada, então:
- A soma dos dígitos de
23
é cinco. - A soma dos dígitos de
43
é sete. - A soma dos dígitos de
8
é oito. - Portanto, a soma total é 20.
Essa é uma maneira de escrever nossa função sum_digits()
:
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = 23, 43, 8
print(sum_digits(numbers))
20
Vamos usar essa função para explorar diferentes maneiras de criar um cache.
Cache manual do Python
Primeiro, vamos criar o cache manualmente. Embora também possamos automatizar isso facilmente, criar um cache manualmente nos ajuda a entender o processo.
Vamos criar um dicionário e adicionar pares de valores-chave sempre que chamarmos a função com um novo valor para armazenar os resultados. Se você chamar a função com um valor que já esteja armazenado nesse dicionário, a função retornará o valor armazenado sem precisar calculá-lo novamente:
import random
import timeit
def sum_digits(numbers):
if numbers not in sum_digits.my_cache:
sum_digits.my_cache[numbers] = sum(
int(digit) for number in numbers for digit in str(number)
)
return sum_digits.my_cache[numbers]
sum_digits.my_cache = {}
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.28875587500078836
0.0044607500021811575
A segunda chamada para sum_digits(numbers)
é muito mais rápida do que a primeira porque usa o valor armazenado em cache.
Agora vamos explicar o código acima com mais detalhes. Primeiro, observe que criamos o dicionário sum_digits.my_cache
depois de definir a função, embora o usemos na definição da função.
A função sum_digits()
verifica se o argumento passado para a função já é uma das chaves do dicionário sum_digits.my_cache
. A soma de todos os dígitos só será avaliada se o argumento ainda não estiver no cache.
Como o argumento que usamos ao chamar a função serve como chave no dicionário, ele deve ser um tipo de dados hashable. Uma lista não é hashable, portanto, não podemos usá-la como chave em um dicionário. Por exemplo, vamos tentar substituir numbers
por uma lista em vez de uma tupla - isso gerará um TypeError
:
# ...
numbers = [random.randint(1, 1000) for _ in range(1_000_000)]
# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
Criar um cache manualmente é ótimo para fins de aprendizado, mas agora vamos explorar maneiras mais rápidas de fazer isso.
Você pode usar o cache do Python com functools.lru_cache()
O Python tem o decorador lru_cache()
desde a versão 3.2. O "lru" no início do nome da função significa "least recently used" (usado menos recentemente). Podemos pensar no cache como uma caixa para armazenar itens usados com frequência. Quando ele fica cheio, a estratégia LRU joga fora o item que não usamos há mais tempo para abrir espaço para algo novo.
Vamos decorar nossa função sum_digits()
com @functools.lru_cache
:
import functools
import random
import timeit
@functools.lru_cache
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.28326129099878017
0.002184917000704445
Graças ao armazenamento em cache, a segunda chamada leva muito menos tempo para ser executada.
Por padrão, o cache armazena os primeiros 128 valores calculados. Quando todos os 128 lugares estiverem cheios, o algoritmo excluirá o valor usado menos recentemente (LRU) para abrir espaço para novos valores.
Podemos definir um tamanho máximo de cache diferente quando decoramos a função usando o parâmetro maxsize
:
import functools
import random
import timeit
@functools.lru_cache(maxsize=5)
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
# ...
Nesse caso, o cache armazena apenas cinco valores. Também podemos definir o argumento maxsize
como None
se não quisermos limitar o tamanho do cache.
Você pode usar o cache do Python com functools.cache()
O Python 3.9 inclui um decorador de cache mais simples e mais rápido -functools.cache()
. Esse decorador tem duas características principais:
- Ele não tem um tamanho máximo - é semelhante a chamar
functools.lru_cache(maxsize=None)
. - Ele armazena todas as chamadas de função e seus resultados (não usa a estratégia LRU). Isso é adequado para funções com saídas relativamente pequenas ou quando não precisamos nos preocupar com as limitações de tamanho do cache.
Vamos usar o decorador @functools.cache
na função sum_digits()
:
import functools
import random
import timeit
@functools.cache
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.16661812500024098
0.0018135829996026587
Decorar sum_digits()
com @functools.cache
é equivalente a atribuir sum_digits
a functools.cache()
:
# ...
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
sum_digits = functools.cache(sum_digits)
Observe que também podemos usar um estilo de importação diferente:
from functools import cache
Dessa forma, podemos decorar nossas funções usando apenas @cache
.
Outras estratégias de cache
As próprias ferramentas do Python implementam a estratégia de cache LRU, em que as entradas usadas menos recentemente são excluídas para abrir espaço para novos valores.
Vamos dar uma olhada em algumas outras estratégias de cache:
- Primeiro a entrar, primeiro a sair (FIFO): Quando o cache está cheio, o primeiro item adicionado é removido para abrir espaço para novos valores. A diferença entre LRU e FIFO é que o LRU mantém no cache os itens usados recentemente, enquanto o FIFO descarta o item mais antigo, independentemente do uso.
- Último a entrar, primeiro a sair (LIFO): O item adicionado mais recentemente é removido quando o cache está cheio. Imagine uma pilha de pratos em uma cafeteria. O prato que colocamos na pilha mais recentemente (último a entrar) é o que será retirado primeiro (primeiro a sair).
- Mais usado recentemente (MRU): O valor que foi usado mais recentemente é descartado quando há necessidade de espaço no cache.
- Substituição aleatória (RR): Essa estratégia descarta aleatoriamente um item para abrir espaço para um novo.
Essas estratégias também podem ser combinadas com medidas do tempo de vida válido - isso se refere a quanto tempo um dado no cache é considerado válido ou relevante. Imagine um artigo de notícias em um cache. Ela pode ser acessada com frequência (o LRU a manteria), mas depois de uma semana, as notícias podem estar desatualizadas.
Cache do Python: Casos de uso comuns
Até agora, usamos exemplos simplistas para fins de aprendizado. No entanto, o armazenamento em cache tem muitas aplicações no mundo real.
Na ciência de dados, muitas vezes executamos operações repetidas em grandes conjuntos de dados. O uso de resultados em cache reduz o tempo e o custo associados à execução dos mesmos cálculos repetidamente nos mesmos conjuntos de dados.
Também podemos usar o cache para salvar recursos externos, como páginas da Web ou bancos de dados. Vamos considerar um exemplo e armazenar em cache um artigo do DataCamp. Mas primeiro precisamos instalar o módulo de terceiros requests
executando a seguinte linha no terminal:
$ python -m pip install requests
Depois que o requests
estiver instalado, você poderá experimentar o código a seguir, que tenta buscar o mesmo artigo do DataCamp duas vezes usando o decorador @lru_cache
:
import requests
from functools import lru_cache
@lru_cache(maxsize=10)
def get_article(url):
print(f"Fetching article from {url}")
response = requests.get(url)
return response.text
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
Fetching article from https://www.datacamp.com/tutorial/decorators-python
<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>...
<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>...
Como observação lateral, truncamos a saída porque ela é muito longa. Observe, no entanto, que somente a primeira chamada para get_article()
imprime a frase Fetching article from {url}
.
Isso ocorre porque a página da Web é acessada somente na primeira vez em que a chamada é feita. O resultado é armazenado no cache da função. Quando solicitamos a mesma página da Web pela segunda vez, os dados armazenados no cache são retornados.
O armazenamento em cache garante que não haja atrasos desnecessários ao buscar os mesmos dados repetidamente. As APIs externas geralmente também têm limites de taxa e custos associados à obtenção de dados. O armazenamento em cache reduz os custos das APIs e a probabilidade de você atingir os limites de taxa.
Outro caso de uso comum é em aplicativos de machine learning em que vários cálculos caros precisam ser repetidos. Por exemplo, se precisarmos tokenizar e vetorizar um texto antes de usá-lo em um modelo de machine learning, poderemos armazenar a saída em um cache. Dessa forma, não precisaremos repetir as operações de alto custo computacional.
Desafios comuns ao armazenar em cache em Python
Você aprendeu sobre as vantagens do armazenamento em cache em Python. Há também alguns desafios e desvantagens que você deve ter em mente ao implementar um cache:
- Invalidação e consistência do cache: Os dados podem mudar com o tempo. Portanto, os valores armazenados em um cache também podem precisar ser atualizados ou removidos.
- Gerenciamento de memória: O armazenamento de grandes quantidades de dados em um cache requer memória, e isso pode causar problemas de desempenho se o cache crescer indefinidamente.
- Complexidade: A adição de caches introduz complexidade ao sistema ao criar e manter o cache. Muitas vezes, os benefícios superam esses custos, mas essa maior complexidade pode levar a bugs difíceis de encontrar e corrigir.
Conclusão
Podemos usar o armazenamento em cache para otimizar o desempenho quando operações computacionalmente intensas são repetidas nos mesmos dados.
O Python tem dois decoradores que permitem que você crie um cache ao chamar funções: @lru_cache
e @cache
no módulo functools
.
No entanto, precisamos garantir que mantenhamos o cache atualizado e que gerenciemos adequadamente a memória.
Se você quiser aprender mais sobre cache e Python, confira este programa de seis cursos de habilidades em programação Python.
Aprenda Python para ciência de dados!
curso
Python Toolbox
curso
Practicing Coding Interview Questions in Python
tutorial
Tutorial de funções Python
tutorial
Tutorial de lambda em Python
DataCamp Team
3 min
tutorial
Otimização em Python: Técnicas, pacotes e práticas recomendadas
tutorial
Tutorial e exemplos de funções e métodos de lista do Python
tutorial
Tutorial de compreensão de dicionário Python
tutorial