Neste artigo, vamos aprender sobre cache em Python. Vamos entender o que é e como usá-lo de forma eficaz.
O cache é uma técnica usada para melhorar o desempenho dos aplicativos, armazenando temporariamente os resultados obtidos pelo programa para reutilizá-los se necessário posteriormente.
Neste tutorial, vamos aprender diferentes técnicas para cache em Python, incluindo os decoradores @lru_cache e @cache no módulo functools.
Para quem está com pressa, vamos começar com uma implementação de cache bem curta e depois continuar com mais detalhes.
Resposta curta: Implementação de cache em Python
Pra criar um cache em Python, a gente pode usar o decorador @cache do módulo functools. No código abaixo, repara que a função print() só é executada uma vez:
import functools
@functools.cachedefsquare(n):print(f"Calculating square of {n}")return n * n
# Testing the cached functionprint(square(4))print(square(4))# Calculating square of 4# 16# 16Tecnologia
Editor Aprenda com exemplos, pressione Tab para entrar. Pressione Escape e depois Tab ou Shift+Tab para sair.
Digamos que precisamos resolver um problema matemático e levamos uma hora pra chegar na resposta certa. 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 cache em Python funciona de um jeito parecido — ele guarda valores quando eles são calculados dentro de chamadas de função pra usar de novo quando precisar. Esse tipo de cache também é conhecido como memoização.
Vamos ver um pequeno exemplo que calcula a soma de um grande intervalo de números duas vezes:
A saída mostra que as duas chamadas levam mais ou menos o mesmo tempo (dependendo da nossa configuração, podemos ter tempos de execução mais rápidos ou mais lentos).
Mas dá pra usar um cache pra não ter que calcular o mesmo valor várias vezes. A gente pode redefinir o nome sum usando a função cache() no módulo functools que já vem instalado:
A segunda chamada agora leva alguns microssegundos em vez de mais de um segundo, porque o resultado da 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 embutida sum(). Só pra constar, um decorador em Python é uma função que mexe no jeito que outra função funciona, sem mexer no código dela de verdade. Você pode aprender mais sobre decoradores neste Tutorial de Decoradores Python.
O decorador functools.cache() foi adicionado ao Python na versão 3.9, mas a gente pode usar functools.lru_cache() para versões mais antigas. Na próxima seção, vamos ver essas duas maneiras de criar um cache, incluindo o uso da notação decoradora mais comum, como @cache.
Cache em Python: Diferentes métodos
O módulo functools do Python tem dois decoradores para aplicar o cache às funções. Vamos dar uma olhada em 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 devolve a soma dos dígitos desses números. Por exemplo, se usarmos 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.
Então, o total é 20.
Essa é uma maneira de escrevermos nossa função sum_digits():
defsum_digits(numbers):returnsum(int(digit)for number in numbers for digit instr(number))
numbers =23,43,8print(sum_digits(numbers))# 20Tecnologia
Editor Aprenda com exemplos, pressione Tab para entrar. Pressione Escape e depois Tab ou Shift+Tab para sair.
Vamos usar essa função para ver diferentes maneiras de criar um cache.
Cache manual em Python
Primeiro, vamos criar o cache manualmente. Embora também pudéssemos automatizar isso facilmente, criar um cache manualmente nos ajuda a entender o processo.
Vamos criar um dicionário e adicionar pares de chave-valor cada vez que chamarmos a função com um novo valor para guardar os resultados. Se chamarmos a função com um valor que já está guardado nesse dicionário, a função vai devolver o valor guardado sem precisar calcular de novo:
import random
import timeit
defsum_digits(numbers):if numbers notin sum_digits.my_cache:
sum_digits.my_cache[numbers]=sum(int(digit)for number in numbers for digit instr(number))return sum_digits.my_cache[numbers]
sum_digits.my_cache ={}
numbers =tuple(random.randint(1,1000)for _ inrange(1_000_000))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))Tecnologia
Editor Aprenda com exemplos, pressione Tab para entrar. Pressione Escape e depois Tab ou Shift+Tab para sair.
A segunda chamada para sum_digits(numbers) é bem mais rápida que a primeira porque usa o valor armazenado em cache.
Vamos agora explicar o código acima com mais detalhes. Primeiro, repara que criamos o dicionário sum_digits.my_cache depois de definir a função, mesmo que a 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 no dicionário sum_digits.my_cache. A soma de todos os dígitos só é calculada 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 precisa ser um tipo de dados hashável. Uma lista não é hashable, então não dá pra usar como chave num dicionário. Por exemplo, vamos tentar substituir numbers por uma lista em vez de uma tupla — isso vai gerar um erro de tipo TypeError:
# ...
numbers = [random.randint(1, 1000) for _ in range(1_000_000)]
# ...Tecnologia
Criar um cache manualmente é ótimo para fins de aprendizagem, mas agora vamos explorar maneiras mais rápidas de fazer isso.
Cache Python com functools.lru_cache()
O Python tem o decorador lru_cache() desde a versão 3.2. O “lru” no começo do nome da função significa “menos usado recentemente”. A gente pode pensar no cache como uma caixa pra guardar coisas que a gente usa bastante — quando ela fica cheia, a estratégia LRU joga fora o item que a gente não usa há mais tempo pra abrir espaço pra algo novo.
Vamos decorar nossa função sum_digits() com @functools.lru_cache:
import functools
import random
import timeit
@functools.lru_cachedefsum_digits(numbers):returnsum(int(digit)for number in numbers for digit instr(number))
numbers =tuple(random.randint(1,1000)for _ inrange(1_000_000))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))Tecnologia
Editor Aprenda com exemplos, pressione Tab para entrar. Pressione Escape e depois Tab ou Shift+Tab para sair.
Graças ao cache, a segunda chamada leva bem menos tempo pra rodar.
Por padrão, o cache guarda os primeiros 128 valores calculados. Quando todos os 128 lugares estiverem ocupados, o algoritmo apaga o valor menos usado recentemente (LRU) para abrir espaço para novos valores.
A gente pode definir um tamanho máximo diferente para o cache 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)
)
# ...Tecnologia
Nesse caso, o cache só guarda cinco valores. Também podemos definir o argumento maxsize como None se não quisermos limitar o tamanho do cache.
Cache Python com functools.cache()
O Python 3.9 tem um decorador de cache mais simples e rápido —functools.cache(). Esse decorador tem duas características principais:
Não tem um tamanho máximo — é tipo chamar functools.lru_cache(maxsize=None).
Ele guarda todas as chamadas de função e seus resultados (não usa a estratégia LRU). Isso é legal para funções com saídas relativamente pequenas ou quando a gente não precisa se preocupar com as limitações do tamanho do cache.
Vamos usar o decorador @functools.cache na função sum_digits():
import functools
import random
import timeit
@functools.cachedefsum_digits(numbers):returnsum(int(digit)for number in numbers for digit instr(number))
numbers =tuple(random.randint(1,1000)for _ inrange(1_000_000))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))print(
timeit.timeit("sum_digits(numbers)",globals=globals(),
number=1))Tecnologia
Editor Aprenda com exemplos, pressione Tab para entrar. Pressione Escape e depois Tab ou Shift+Tab para sair.
Decorar sum_digits() com @functools.cache é o mesmo que colocar sum_digits em 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)Tecnologia
Observe que também podemos usar um estilo de importação diferente:
Assim, podemos decorar nossas funções usando só um @cache.
Outras estratégias de armazenamento em cache
As próprias ferramentas do Python usam a estratégia de cache LRU, onde as entradas menos usadas são apagadas pra dar espaço pra novos valores.
Vamos dar uma olhada em algumas outras estratégias de cache:
Primeiro a entrar, primeiro a sair (FIFO): Quando o cache fica 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 os itens usados recentemente no cache, 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 fica cheio. Imagina uma pilha de pratos numa cafeteria. A placa que colocamos na pilha mais recentemente (última a entrar) é a que vamos tirar primeiro (primeira a sair).
Mais recentemente usado (MRU): O valor que foi usado mais recentemente é descartado quando é preciso espaço no cache.
Substituição aleatória (RR): Essa estratégia descarta um item aleatoriamente para abrir espaço para um novo.
Essas estratégias também podem ser combinadas com medidas de tempo de vida válido — isso se refere ao tempo durante o qual um dado no cache é considerado válido ou relevante. Imagina uma notícia em um cache. Pode ser acessado com frequência (o LRU o manteria), mas depois de uma semana, a notícia pode estar desatualizada.
Cache em Python: Casos de uso comuns
Até agora, usamos exemplos simples pra facilitar o aprendizado. Mas o cache tem várias aplicações na vida real.
Na ciência de dados, a gente costuma fazer operações repetidas em grandes conjuntos de dados. Usar resultados em cache reduz o tempo e o custo de fazer os mesmos cálculos várias vezes 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 ver um exemplo e armazenar um artigo do DataCamp no cache. Mas primeiro precisamos instalar o módulo de terceiros requests executando a seguinte linha no terminal:
Só pra constar, a gente cortou a saída porque era muito longa. Mas, repara que só a primeira chamada para get_article() mostra a frase Fetching article from {url}.
Isso porque a página da web só é acessada na primeira vez que a chamada é feita. O resultado fica guardado no cache da função. Quando a gente pede a mesma página pela segunda vez, os dados que estão guardados no cache são mostrados.
O cache garante que não haja atrasos desnecessários ao buscar os mesmos dados várias vezes. As APIs externas também costumam ter limites de taxa e custos associados à obtenção de dados. O cache reduz os custos das APIs e a chance de atingir os limites de taxa.
Outro caso de uso comum é em aplicativos de machine learning, onde 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, podemos guardar o resultado em um cache. Assim, não precisaremos repetir as operações que exigem muito processamento.
Desafios comuns ao usar cache em Python
A gente aprendeu sobre as vantagens do cache no Python. Também tem alguns desafios e desvantagens que você precisa ter em mente ao implementar um cache:
Invalidação e consistência do cache: Os dados podem mudar com o tempo. Então, os valores guardados no cache também podem precisar ser atualizados ou removidos.
Gerenciamento de memória: Armazenar grandes quantidades de dados em um cache requer memória, e isso pode causar problemas de desempenho se o cache crescer indefinidamente.
Complexidade: Adicionar caches deixa o sistema mais complicado na hora de criar e manter o cache. Muitas vezes, os benefícios superam esses custos, mas essa complexidade maior pode causar erros difíceis de encontrar e corrigir.
Conclusão
Podemos usar o cache para otimizar o desempenho quando operações computacionalmente intensivas são repetidas nos mesmos dados.
Python tem dois decoradores pra criar um cache quando chamar funções: @lru_cache e @cache no módulo functools.
Precisamos garantir, no entanto, que a cache esteja sempre atualizada e que a memória seja gerenciada corretamente.
Se você quiser saber mais sobre cache e Python, dá uma olhada neste programa de seis módulos sobre programação em Python.
Estudei Física e Matemática em nível superior na Universidade de Malta. Depois, mudei-me para Londres e fiz meu doutorado em Física no Imperial College. Trabalhei em novas técnicas ópticas para obter imagens da retina humana. Agora, meu foco é escrever sobre Python, comunicar sobre Python e ensinar Python.
Descubra como escrever funções em Python reutilizáveis e eficientes. Domine parâmetros, instruções de retorno e temas avançados como funções lambda. Organize melhor seu código com main() e outras boas práticas.
Este artigo ensina a você sobre otimização numérica, destacando diferentes técnicas. Ele discute os pacotes Python, como SciPy, CVXPY e Pyomo, e fornece um notebook DataLab prático para você executar exemplos de código.
Aprenda sobre as funções lambda do Python, para que servem e quando usá-las. Inclui exemplos práticos e melhores práticas para uma implementação eficaz.
Neste artigo, vamos aprender sobre cache em Python. Vamos entender o que é e como usá-lo de forma eficaz.
O cache é uma técnica usada para melhorar o desempenho dos aplicativos, armazenando temporariamente os resultados obtidos pelo programa para reutilizá-los se necessário posteriormente.
Neste tutorial, vamos aprender diferentes técnicas para cache em Python, incluindo os decoradores
@lru_cachee@cacheno módulofunctools.Para quem está com pressa, vamos começar com uma implementação de cache bem curta e depois continuar com mais detalhes.
Resposta curta: Implementação de cache em Python
Pra criar um cache em Python, a gente pode usar o decorador
@cachedo módulofunctools. No código abaixo, repara que a funçãoprint()só é executada uma vez:O que é cache em Python?
Digamos que precisamos resolver um problema matemático e levamos uma hora pra chegar na resposta certa. 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 cache em Python funciona de um jeito parecido — ele guarda valores quando eles são calculados dentro de chamadas de função pra usar de novo quando precisar. Esse tipo de cache também é conhecido como memoização.
Vamos ver um pequeno exemplo que calcula a soma de um grande intervalo de números duas vezes:
O programa precisa calcular a soma toda vez. A gente pode confirmar isso cronometrando as duas chamadas:
A saída mostra que as duas chamadas levam mais ou menos o mesmo tempo (dependendo da nossa configuração, podemos ter tempos de execução mais rápidos ou mais lentos).
Mas dá pra usar um cache pra não ter que calcular o mesmo valor várias vezes. A gente pode redefinir o nome
sumusando a funçãocache()no módulofunctoolsque já vem instalado:A segunda chamada agora leva alguns microssegundos em vez de mais de um segundo, porque o resultado da 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 embutidasum(). Só pra constar, um decorador em Python é uma função que mexe no jeito que outra função funciona, sem mexer no código dela de verdade. Você pode aprender mais sobre decoradores neste Tutorial de Decoradores Python.O decorador
functools.cache()foi adicionado ao Python na versão 3.9, mas a gente pode usarfunctools.lru_cache()para versões mais antigas. Na próxima seção, vamos ver essas duas maneiras de criar um cache, incluindo o uso da notação decoradora mais comum, como@cache.Cache em Python: Diferentes métodos
O módulo
functoolsdo Python tem dois decoradores para aplicar o cache às funções. Vamos dar uma olhada emfunctools.lru_cache()efunctools.cache()com um exemplo.Vamos escrever uma função
sum_digits()que recebe uma sequência de números e devolve a soma dos dígitos desses números. Por exemplo, se usarmos a tupla(23, 43, 8)como entrada, então:23é cinco.43é sete.8é oito.Essa é uma maneira de escrevermos nossa função
sum_digits():Vamos usar essa função para ver diferentes maneiras de criar um cache.
Cache manual em Python
Primeiro, vamos criar o cache manualmente. Embora também pudéssemos automatizar isso facilmente, criar um cache manualmente nos ajuda a entender o processo.
Vamos criar um dicionário e adicionar pares de chave-valor cada vez que chamarmos a função com um novo valor para guardar os resultados. Se chamarmos a função com um valor que já está guardado nesse dicionário, a função vai devolver o valor guardado sem precisar calcular de novo:
A segunda chamada para
sum_digits(numbers)é bem mais rápida que a primeira porque usa o valor armazenado em cache.Vamos agora explicar o código acima com mais detalhes. Primeiro, repara que criamos o dicionário
sum_digits.my_cachedepois de definir a função, mesmo que a 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 no dicionáriosum_digits.my_cache. A soma de todos os dígitos só é calculada 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 precisa ser um tipo de dados hashável. Uma lista não é hashable, então não dá pra usar como chave num dicionário. Por exemplo, vamos tentar substituir
numberspor uma lista em vez de uma tupla — isso vai gerar um erro de tipoTypeError:Criar um cache manualmente é ótimo para fins de aprendizagem, mas agora vamos explorar maneiras mais rápidas de fazer isso.
Cache Python com functools.lru_cache()
O Python tem o decorador
lru_cache()desde a versão 3.2. O “lru” no começo do nome da função significa “menos usado recentemente”. A gente pode pensar no cache como uma caixa pra guardar coisas que a gente usa bastante — quando ela fica cheia, a estratégia LRU joga fora o item que a gente não usa há mais tempo pra abrir espaço pra algo novo.Vamos decorar nossa função
sum_digits()com@functools.lru_cache:Graças ao cache, a segunda chamada leva bem menos tempo pra rodar.
Por padrão, o cache guarda os primeiros 128 valores calculados. Quando todos os 128 lugares estiverem ocupados, o algoritmo apaga o valor menos usado recentemente (LRU) para abrir espaço para novos valores.
A gente pode definir um tamanho máximo diferente para o cache quando decoramos a função usando o parâmetro
maxsize:Nesse caso, o cache só guarda cinco valores. Também podemos definir o argumento
maxsizecomoNonese não quisermos limitar o tamanho do cache.Cache Python com functools.cache()
O Python 3.9 tem um decorador de cache mais simples e rápido —
functools.cache(). Esse decorador tem duas características principais:functools.lru_cache(maxsize=None).Vamos usar o decorador
@functools.cachena funçãosum_digits():Decorar
sum_digits()com@functools.cacheé o mesmo que colocarsum_digitsemfunctools.cache():Observe que também podemos usar um estilo de importação diferente:
Assim, podemos decorar nossas funções usando só um
@cache.Outras estratégias de armazenamento em cache
As próprias ferramentas do Python usam a estratégia de cache LRU, onde as entradas menos usadas são apagadas pra dar espaço pra novos valores.
Vamos dar uma olhada em algumas outras estratégias de cache:
Essas estratégias também podem ser combinadas com medidas de tempo de vida válido — isso se refere ao tempo durante o qual um dado no cache é considerado válido ou relevante. Imagina uma notícia em um cache. Pode ser acessado com frequência (o LRU o manteria), mas depois de uma semana, a notícia pode estar desatualizada.
Cache em Python: Casos de uso comuns
Até agora, usamos exemplos simples pra facilitar o aprendizado. Mas o cache tem várias aplicações na vida real.
Na ciência de dados, a gente costuma fazer operações repetidas em grandes conjuntos de dados. Usar resultados em cache reduz o tempo e o custo de fazer os mesmos cálculos várias vezes 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 ver um exemplo e armazenar um artigo do DataCamp no cache. Mas primeiro precisamos instalar o módulo de terceiros
requestsexecutando a seguinte linha no terminal:Depois de instalar o
requests, podemos tentar o seguinte código, que tenta buscar o mesmo artigo do DataCamp duas vezes usando o decorador@lru_cache:Só pra constar, a gente cortou a saída porque era muito longa. Mas, repara que só a primeira chamada para
get_article()mostra a fraseFetching article from {url}.Isso porque a página da web só é acessada na primeira vez que a chamada é feita. O resultado fica guardado no cache da função. Quando a gente pede a mesma página pela segunda vez, os dados que estão guardados no cache são mostrados.
O cache garante que não haja atrasos desnecessários ao buscar os mesmos dados várias vezes. As APIs externas também costumam ter limites de taxa e custos associados à obtenção de dados. O cache reduz os custos das APIs e a chance de atingir os limites de taxa.
Outro caso de uso comum é em aplicativos de machine learning, onde 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, podemos guardar o resultado em um cache. Assim, não precisaremos repetir as operações que exigem muito processamento.
Desafios comuns ao usar cache em Python
A gente aprendeu sobre as vantagens do cache no Python. Também tem alguns desafios e desvantagens que você precisa ter em mente ao implementar um cache:
Conclusão
Podemos usar o cache para otimizar o desempenho quando operações computacionalmente intensivas são repetidas nos mesmos dados.
Python tem dois decoradores pra criar um cache quando chamar funções:
@lru_cachee@cacheno módulofunctools.Precisamos garantir, no entanto, que a cache esteja sempre atualizada e que a memória seja gerenciada corretamente.
Se você quiser saber mais sobre cache e Python, dá uma olhada neste programa de seis módulos sobre programação em Python.