Link Search Menu Expand Document

Caching de Funciones

El cach茅 es un t茅rmino muy usado en inform谩tica, y hace referencia al almacenamiento de resultados previos para su posterior reutilizaci贸n, lo que permite reducir el tiempo de respuesta. Por ejemplo, si llamamos a una funci贸n con un determinado par谩metro y acto seguido realizamos la misma llamada, ser铆a interesante reutilizar el primer resultado para no tener que calcularlo otra vez. Existen por lo tanto dos posibilidades:

  • Si ejecutamos la funci贸n y el resultado no ha sido calculado con anterioridad, se calcula y se almacena por si fuera 煤til en el futuro. Esto se conoce como cache miss.
  • Si ejecutamos la funci贸n y el cach茅 tiene almacenado el resultado para esa operaci贸n, en vez de calcular otra vez la salida la podemos reutilizar, lo que se conoce como cache hit. Dado que estamos reutilizando un valor ya calculado, generalmente el tiempo de respuesta ser谩 menor.

Por suerte, Python nos permite a帽adir caching a nuestras funciones, pero antes de implementarlo es conveniente hacer un an谩lisis sobre nuestro programa y determinar si merece la pena. Algunas cosas a tener en cuenta:

  • El caching es especialmente 煤til cuando trabajamos con funciones muy intensivas en c谩lculo, lo que hace que reutilizar el valor del cach茅 reduzca notablemente el tiempo de respuesta.
  • Es necesario conocer (a nivel estad铆stico) la distribuci贸n de los argumentos con los que se llama la funci贸n. Si la funci贸n bajo estudio se llama con valores muy dispares y apenas repetidos, el caching poco ayudar谩, ya que apenas tendremos un cache hit.
  • El uso de un cach茅 puede mejorar el tiempo de respuesta, pero frecuentemente se paga en un incremento del uso de memoria. Tambi茅n es necesario decidir el n煤mero de valores a almacenar.

A continuaci贸n veremos como implementar caching en Python, pudiendo hacerlo con diccionarios o utilizando la librer铆a functools. Para ejemplificarlo, veremos como implementar un cach茅 en nuestro c贸digo de n煤meros primos visto anteriormente, empleando ambas formas.

def es_primo(num):
    for n in range(2, num):
        if num % n == 0:
            return False
    return True

Caching con Diccionarios

La primera forma de realizarlo es usando un diccionario como cach茅. N贸tese que este es un ejemplo did谩ctico, y que obvia algunos factores. Como puedes ver tenemos claramente diferenciado el cache hit y el cache miss. Si el valor no est谩 en el cach茅 se calcula y se devuelve.

def es_primo_concache(num, _cache={}):
    if num not in _cache:
        _cache[num] = True
        for n in range(2, num):
            if num % n == 0:
                _cache[num] = False
                break
    return _cache[num]

Dado que es_primo es bastante intensivo en c谩lculo, cuando usamos n煤meros grandes el ahorro puede ser muy significativo. En el siguiente c贸digo podemos ver como la primera vez que ejecutamos la funci贸n, se tardan 3.5 segundos, ya que el resultado tiene que ser calculado. Sin embargo la segunda vez que la llamamos con la misma entrada, tenemos un cache hit, por lo que el valor ya no es calculado sino recuperado del cach茅, tardando microsegundos.


import time
tic = time.time()
es_primo_concache(25565479)
print(time.time() - tic)

tic = time.time()
es_primo_concache(25565479)
print(time.time() - tic)

# 3.5551438331604004
# 4.0531158447265625e-06

Caching con functools y lru_cache

La segunda forma de realizarlo, y un poco m谩s sofisticada es usando lru_cache, un decorador que viene con la librer铆a est谩ndar functools. La mayor ventaja es que no necesitamos modificar la funci贸n. N贸tese que maxsize nos permite indicar el n煤mero m谩ximo de valores que queremos almacenar en el cach茅.

from functools import lru_cache

@lru_cache(maxsize=32)
def es_primo_concache(num):
    for n in range(2, num):
        if num % n == 0:
            return False
    return True

Por lo tanto si ahora llamamos a nuestra funci贸n con los mismos valores, podemos ver como la primera vez tarda 3.9 segundos, pero la segunda apenas tarda unos microsegundos.

import time
tic = time.time()
es_primo_concache(25565479)
print(time.time() - tic)

tic = time.time()
es_primo_concache(25565479)
print(time.time() - tic)

# 3.9316678047180176
# 3.0994415283203125e-06

En el caso de que queramos limpiar el cach茅 de nuestra funci贸n, podemos realizar lo siguiente.

es_primo_concache.cache_clear()