Pular para o conteúdo principal

Cache do Python: Dois métodos simples

Aprenda a usar decoradores como @functools.lru_cache ou @functools.cache para armazenar funções em cache no Python.
Actualizado 30 de jul. de 2024  · 12 min de leitura

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.

Temas

Aprenda Python para ciência de dados!

curso

Introduction to Functions in Python

3 hr
419.7K
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
Ver DetalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow
Relacionado

tutorial

Tutorial de funções Python

Um tutorial sobre funções em Python que aborda como escrever funções, como chamá-las e muito mais!

Karlijn Willems

14 min

tutorial

Tutorial de lambda em Python

Aprenda uma maneira mais rápida de escrever funções em tempo real com as funções lambda.
DataCamp Team's photo

DataCamp Team

3 min

tutorial

Otimização em Python: Técnicas, pacotes e práticas recomendadas

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.
Kurtis Pykes 's photo

Kurtis Pykes

19 min

tutorial

Tutorial e exemplos de funções e métodos de lista do Python

Saiba mais sobre as funções e os métodos da Lista do Python. Siga exemplos de código para list() e outras funções e métodos Python agora!
Abid Ali Awan's photo

Abid Ali Awan

7 min

tutorial

Tutorial de compreensão de dicionário Python

Saiba tudo sobre a compreensão de dicionário do Python: como você pode usá-la para criar dicionários, substituir loops for (aninhados) ou funções lambda por map(), filter() e reduce(), ...!
Sejal Jaiswal's photo

Sejal Jaiswal

14 min

tutorial

21 ferramentas essenciais do Python

Aprenda sobre as ferramentas Python essenciais para o desenvolvimento de software, raspagem e desenvolvimento da Web, análise e visualização de dados e aprendizado de máquina.
Abid Ali Awan's photo

Abid Ali Awan

6 min

See MoreSee More