Saltar al contenido principal
InicioTutorialesPython

Caché de Python: Dos métodos sencillos

Aprende a utilizar decoradores como @functools.lru_cache o @functools.cache para almacenar en caché funciones en Python.
Actualizado 30 jul 2024  · 12 min leer

En este artículo, aprenderemos sobre el almacenamiento en caché en Python. Entenderemos qué es y cómo utilizarlo eficazmente.

El almacenamiento en caché es una técnica utilizada para mejorar el rendimiento de las aplicaciones, almacenando temporalmente los resultados obtenidos por el programa para reutilizarlos si se necesitan más adelante.

En este tutorial, aprenderemos diferentes técnicas de almacenamiento en caché en Python, incluidos los decoradores @lru_cache y @cache del módulo functools.

Para los que tengáis prisa, empecemos con una implementación muy breve de la caché y luego continuemos con más detalles.

Respuesta corta: Implementación de caché en Python

Para crear una caché en Python, podemos utilizar el decorador @cache del módulo functools. En el código siguiente, observa que la función print() sólo se ejecuta una 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

¿Qué es el almacenamiento en caché en Python?

Supongamos que tenemos que resolver un problema matemático y dedicamos una hora a obtener la respuesta correcta. Si tuviéramos que resolver el mismo problema al día siguiente, sería útil reutilizar nuestro trabajo anterior en lugar de empezar de nuevo.

El almacenamiento en caché en Python sigue un principio similar: almacena valores cuando se calculan dentro de llamadas a funciones para reutilizarlos cuando vuelvan a necesitarse. Este tipo de almacenamiento en caché también se denomina memoización.

Veamos un breve ejemplo que calcula dos veces la suma de un gran intervalo de números:

output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)
5000000050000000
5000000050000000

El programa tiene que calcular la suma cada vez. Podemos confirmarlo cronometrando las dos llamadas:

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

La salida muestra que ambas llamadas tardan aproximadamente lo mismo (dependiendo de nuestra configuración, podemos obtener tiempos de ejecución más rápidos o más lentos).

Sin embargo, podemos utilizar una caché para evitar calcular el mismo valor más de una vez. Podemos redefinir el nombre sum utilizando la función cache() del módulo incorporado functools:

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

La segunda llamada tarda ahora un par de microsegundos en lugar de más de un segundo, porque el resultado de hallar la suma de los números de 0 a 100.000.000 ya se ha calculado y almacenado en caché: la segunda llamada utiliza el valor que se calculó y almacenó anteriormente.

Arriba, utilizamos el decorador functools.cache() para incluir una caché a la función incorporada sum(). Como nota al margen, un decorador en Python es una función que modifica el comportamiento de otra función sin cambiar permanentemente su código. Puedes aprender más sobre los decoradores en este tutorial sobre decoradores de Python.

El decorador functools.cache() se añadió a Python en la versión 3.9, pero podemos utilizar functools.lru_cache() para versiones anteriores. En la siguiente sección, exploraremos estas dos formas de crear una caché, incluyendo el uso de la notación de decorador más utilizada, como @cache.

Caché en Python: Diferentes métodos

El módulo functools de Python tiene dos decoradores para aplicar la caché a las funciones. Exploremos functools.lru_cache() y functools.cache() con un ejemplo.

Escribamos una función sum_digits() que tome una secuencia de números y devuelva la suma de los dígitos de esos números. Por ejemplo, si utilizamos la tupla (23, 43, 8) como entrada, entonces

  • La suma de los dígitos de 23 es cinco.
  • La suma de los dígitos de 43 es siete.
  • La suma de los dígitos de 8 es ocho.
  • Por tanto, la suma total es 20.

Ésta es una forma de escribir nuestra función 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

Utilicemos esta función para explorar distintas formas de crear una caché.

Caché manual Python

Primero vamos a crear la caché manualmente. Aunque también podríamos automatizarlo fácilmente, crear una caché manualmente nos ayuda a comprender el proceso.

Vamos a crear un diccionario y a añadir pares clave-valor cada vez que llamemos a la función con un nuevo valor para almacenar los resultados. Si llamamos a la función con un valor que ya está almacenado en este diccionario, la función devolverá el valor almacenado sin elaborarlo de nuevo:

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

La segunda llamada a sum_digits(numbers) es mucho más rápida que la primera porque utiliza el valor almacenado en caché.

Ahora vamos a explicar el código anterior con más detalle. En primer lugar, fíjate en que creamos el diccionario sum_digits.my_cache después de definir la función, aunque lo utilicemos en la definición de la función.

La función sum_digits() comprueba si el argumento pasado a la función es ya una de las claves del diccionario sum_digits.my_cache. La suma de todos los dígitos sólo se evalúa si el argumento no está ya en la caché.

Como el argumento que utilizamos al llamar a la función sirve como clave en el diccionario, debe ser un tipo de dato hashable. Una lista no es hashable, por lo que no podemos utilizarla como clave en un diccionario. Por ejemplo, intentemos sustituir numbers por una lista en lugar de una tupla, lo que provocará un TypeError:

# ...

numbers = [random.randint(1, 1000) for _ in range(1_000_000)]

# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'

Crear una caché manualmente está muy bien para aprender, pero ahora vamos a explorar formas más rápidas de hacerlo.

Caché en Python con functools.lru_cache()

Python dispone del decorador lru_cache() desde la versión 3.2. El "lru" al principio del nombre de la función significa "menos utilizado recientemente". Podemos pensar en la caché como una caja para almacenar cosas que se utilizan con frecuencia: cuando se llena, la estrategia LRU tira el elemento que no hemos utilizado en más tiempo para dejar espacio a algo nuevo.

Vamos a decorar nuestra función sum_digits() con @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

Gracias al almacenamiento en caché, la segunda llamada tarda mucho menos en ejecutarse.

Por defecto, la caché almacena los 128 primeros valores calculados. Una vez que las 128 plazas están llenas, el algoritmo borra el valor utilizado menos recientemente (LRU) para dejar espacio a nuevos valores.

Podemos establecer un tamaño máximo de caché diferente cuando decoramos la función utilizando el 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)
    )

# ...

En este caso, la caché sólo almacena cinco valores. También podemos establecer el argumento maxsize en None si no queremos limitar el tamaño de la caché.

Caché en Python con functools.cache()

Python 3.9 incluye un decorador de caché más sencillo y rápido-functools.cache(). Este decorador tiene dos características principales:

  • No tiene un tamaño máximo: es similar a llamar a functools.lru_cache(maxsize=None).
  • Almacena todas las llamadas a funciones y sus resultados (no utiliza la estrategia LRU). Esto es adecuado para funciones con salidas relativamente pequeñas o cuando no necesitamos preocuparnos por las limitaciones de tamaño de la caché.

Utilicemos el decorador @functools.cache en la función 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() con @functools.cache equivale a asignar 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)

Ten en cuenta que también podemos utilizar un estilo de importación diferente:

from functools import cache

De este modo, podemos decorar nuestras funciones utilizando sólo @cache.

Otras estrategias de almacenamiento en caché

Las propias herramientas de Python implementan la estrategia de caché LRU, en la que las entradas utilizadas menos recientemente se eliminan para dejar espacio a los nuevos valores.

Veamos otras estrategias de almacenamiento en caché:

  • Primero en entrar, primero en salir (FIFO): Cuando la caché está llena, el primer elemento añadido se elimina para dejar espacio a los nuevos valores. La diferencia entre LRU y FIFO es que LRU mantiene en la caché los elementos utilizados recientemente, mientras que FIFO descarta el elemento más antiguo independientemente de su uso.
  • Último en entrar, primero en salir (LIFO): El elemento añadido más recientemente se elimina cuando la caché está llena. Imagina una pila de platos en una cafetería. La placa que hemos puesto en la pila más recientemente (última en entrar) es la que quitaremos primero (primera en salir).
  • Utilizado más recientemente (MRU): El valor que se ha utilizado más recientemente se descarta cuando se necesita espacio en la caché.
  • Reemplazo aleatorio (RR): Esta estrategia descarta aleatoriamente un objeto para dejar espacio a uno nuevo.

Estas estrategias también pueden combinarse con medidas de la vida útil válida, que se refiere al tiempo que un dato de la caché se considera válido o relevante. Imagina un artículo en una caché. Puede que se acceda a ella con frecuencia (LRU la mantendría), pero al cabo de una semana, la noticia podría estar obsoleta.

Caché en Python: Casos de uso habituales

Hasta ahora, hemos utilizado ejemplos simplistas con fines didácticos. Sin embargo, el almacenamiento en caché tiene muchas aplicaciones en el mundo real.

En la ciencia de datos, a menudo ejecutamos operaciones repetidas en grandes conjuntos de datos. El uso de resultados almacenados en caché reduce el tiempo y el coste asociados a la realización repetida de los mismos cálculos en los mismos conjuntos de datos.

También podemos utilizar la caché para guardar recursos externos, como páginas web o bases de datos. Veamos un ejemplo y guardemos en caché un artículo de DataCamp. Pero antes tendremos que instalar el módulo de terceros requests ejecutando la siguiente línea en el terminal:

$ python -m pip install requests

Una vez instalado requests, podemos probar el siguiente código, que intenta obtener el mismo artículo de DataCamp dos veces utilizando el 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 nota al margen, hemos truncado la salida porque es muy larga. Observa, sin embargo, que sólo la primera llamada a get_article() imprime la frase Fetching article from {url}.

Esto se debe a que sólo se accede a la página web la primera vez que se realiza la llamada. El resultado se almacena en la caché de la función. Cuando solicitamos la misma página web por segunda vez, se devuelven en su lugar los datos almacenados en la caché.

El almacenamiento en caché garantiza que no se produzcan retrasos innecesarios al obtener los mismos datos repetidamente. Las API externas también suelen tener límites de velocidad y costes asociados a la obtención de datos. El almacenamiento en caché reduce los costes de las API y la probabilidad de llegar a los límites de velocidad.

Otro caso de uso común es el de las aplicaciones de aprendizaje automático , en las que es necesario repetir varios cálculos costosos. Por ejemplo, si necesitamos tokenizar y vectorizar un texto antes de utilizarlo en un modelo de aprendizaje automático, podemos almacenar el resultado en una caché. De este modo no tendremos que repetir las operaciones de alto coste computacional.

Desafíos comunes al almacenar en caché en Python

Hemos aprendido las ventajas del almacenamiento en caché en Python. También hay que tener en cuenta algunos retos e inconvenientes al implantar una caché:

  • Invalidación y coherencia de la caché: Los datos pueden cambiar con el tiempo. Por lo tanto, también puede ser necesario actualizar o eliminar los valores almacenados en una caché.
  • Gestión de la memoria: Almacenar grandes cantidades de datos en una caché requiere memoria, y esto puede causar problemas de rendimiento si la caché crece indefinidamente.
  • Complejidad: Añadir cachés introduce complejidad en el sistema al crear y mantener la caché. A menudo, los beneficios superan a estos costes, pero esta mayor complejidad podría dar lugar a errores difíciles de encontrar y corregir.

Conclusión

Podemos utilizar la memoria caché para optimizar el rendimiento cuando se repiten operaciones de cálculo intensivo sobre los mismos datos.

Python dispone de dos decoradores para crear una caché al llamar a funciones: @lru_cache y @cache en el módulo functools.

Sin embargo, debemos asegurarnos de que mantenemos actualizada la caché y gestionamos adecuadamente la memoria.

Si quieres aprender más sobre caché y Python, echa un vistazo a este curso de seis cursos sobre programación en Python.

Temas

¡Aprende Python para la ciencia de datos!

Certificación disponible

Course

Introducción a las funciones en Python

3 hr
416.6K
Aprende el arte de escribir tus propias funciones en Python, así como conceptos clave como el alcance y la gestión de errores.
See DetailsRight Arrow
Start Course
Ver másRight Arrow
Relacionado

tutorial

Tutorial de funciones de Python

Un tutorial sobre funciones en Python que cubre cómo escribir funciones, cómo invocarlas y mucho más.

Karlijn Willems

14 min

tutorial

Tutorial y Ejemplos de Funciones y Métodos de Listas en Python

Conozca las funciones y métodos de lista de Python. Siga ahora ejemplos de código de list() y otras funciones y métodos de Python.
Abid Ali Awan's photo

Abid Ali Awan

7 min

tutorial

Tutorial sobre cómo trabajar con módulos en Python

Los módulos te permiten dividir partes de tu programa en archivos diferentes para facilitar el mantenimiento y mejorar el rendimiento.

Nishant Kumar

8 min

tutorial

21 herramientas esenciales de Python

Conozca las herramientas esenciales de Python para el desarrollo de software, raspado y desarrollo web, análisis y visualización de datos y aprendizaje automático.
Abid Ali Awan's photo

Abid Ali Awan

6 min

tutorial

Tutorial de comprensión del diccionario Python

¡Aprende todo sobre la comprensión de diccionarios en Python: cómo puedes utilizarla para crear diccionarios, para sustituir los for loops (anidados) o las funciones lambda por map(), filter() y reduce(), ...!
Sejal Jaiswal's photo

Sejal Jaiswal

14 min

tutorial

Las mejores técnicas para gestionar valores perdidos que todo científico de datos debe conocer

Explore varias técnicas para manejar eficazmente los valores perdidos y sus implementaciones en Python.
Zoumana Keita 's photo

Zoumana Keita

15 min

See MoreSee More