Programmazione funzionale in Python: usare <strong>functools.lru_cache</strong> per ottimizzare funzioni pure

by theArchitect
SHARE
Programmazione funzionale in Python: usare <strong>functools.lru_cache</strong> per ottimizzare funzioni pure
© Guida-HTML5.it

Introduzione

Quando si parla di programmazione funzionale in Python, spesso ci si concentra su funzioni pure, funzioni di ordine superiore, map, filter e reduce. Un sotto-argomento molto utile nella pratica, però, è la memoizzazione: cioè il salvataggio dei risultati già calcolati per evitare di ripetere computazioni costose.

In Python, il modo più semplice e standard per farlo è usare functools.lru_cache. Questa decorazione è perfetta per funzioni deterministiche, cioè funzioni che, a parità di input, restituiscono sempre lo stesso output. È un concetto molto vicino alla programmazione funzionale, perché valorizza la purezza della funzione e riduce gli effetti collaterali di calcoli ripetuti.

In questo tutorial vedremo come funziona lru_cache, quando usarla, quali vantaggi offre e quali errori evitare. Useremo un esempio concreto: il calcolo ricorsivo di una sequenza numerica, un caso classico in cui la memoizzazione cambia davvero le prestazioni.

Codice completo

from functools import lru_cache
import time


# Versione senza memoizzazione
def fibonacci_senza_cache(n):
    if n < 2:
        return n
    return fibonacci_senza_cache(n - 1) + fibonacci_senza_cache(n - 2)


# Versione con memoizzazione automatica
@lru_cache(maxsize=None)
def fibonacci_con_cache(n):
    if n < 2:
        return n
    return fibonacci_con_cache(n - 1) + fibonacci_con_cache(n - 2)


def misura_tempo(funzione, n):
    start = time.perf_counter()
    risultato = funzione(n)
    end = time.perf_counter()
    return risultato, end - start


if __name__ == "__main__":
    n = 35

    risultato1, tempo1 = misura_tempo(fibonacci_senza_cache, n)
    risultato2, tempo2 = misura_tempo(fibonacci_con_cache, n)

    print(f"Fibonacci senza cache: {risultato1} calcolato in {tempo1:.4f} secondi")
    print(f"Fibonacci con cache:    {risultato2} calcolato in {tempo2:.4f} secondi")

    # Informazioni utili sulla cache
    print("Statistiche cache:", fibonacci_con_cache.cache_info())

    # Pulizia della cache, se serve
    fibonacci_con_cache.cache_clear()
    print("Cache svuotata:", fibonacci_con_cache.cache_info())

Spiegazione

Il modulo functools offre strumenti molto utili per scrivere codice più espressivo e riutilizzabile. Tra questi, lru_cache è uno dei più pratici quando si lavora con funzioni pure e calcoli costosi.

Che cosa fa lru_cache

lru_cache significa Least Recently Used cache. In pratica, memorizza i risultati delle chiamate a una funzione. Se la funzione viene richiamata con gli stessi argomenti, Python non ricalcola il risultato: lo recupera dalla cache.

Nel nostro esempio, fibonacci_con_cache calcola la sequenza di Fibonacci in modo ricorsivo. Senza cache, la stessa sotto-problematica viene ricalcolata molte volte. Con la cache, ogni valore viene salvato e riutilizzato, riducendo drasticamente il numero di chiamate.

Perché è un concetto vicino alla programmazione funzionale

La programmazione funzionale punta spesso su funzioni:

  • pure, cioè senza effetti collaterali;
  • deterministiche, cioè con output prevedibile;
  • componibili, cioè facili da combinare con altre funzioni.

La memoizzazione funziona bene proprio con funzioni di questo tipo, perché il risultato dipende solo dagli argomenti di input. Se la funzione legge variabili globali, usa lo stato del sistema o modifica dati esterni, la cache può diventare sbagliata o fuorviante.

Analisi del codice

Vediamo i punti principali:

  • @lru_cache(maxsize=None): il decoratore attiva la cache. Con maxsize=None la cache non ha limite e conserva tutti i risultati.
  • fibonacci_senza_cache: versione classica, semplice ma inefficiente per valori grandi.
  • fibonacci_con_cache: stessa logica, ma con risultati memorizzati automaticamente.
  • cache_info(): restituisce statistiche utili, come hit, miss e dimensione della cache.
  • cache_clear(): svuota la cache quando serve, per esempio nei test o dopo un cambio di contesto.

Perché l’esempio è importante

Il Fibonacci ricorsivo è un classico esempio didattico perché mette in evidenza il problema della ripetizione. Ma il vero valore di lru_cache emerge in contesti reali, per esempio:

  • calcolo di configurazioni o permutazioni;
  • validazioni ripetute su strutture dati costose da analizzare;
  • trasformazioni pure su dati immutabili;
  • funzioni che eseguono parsing o normalizzazioni ripetitive.

Un vantaggio importante è che non devi scrivere tu la logica di caching: Python la gestisce in modo semplice e affidabile.

Best practice

Usare lru_cache è semplice, ma va fatto con criterio. Ecco le regole più importanti.

  • Usala solo con funzioni pure: se il risultato dipende solo dagli argomenti, la cache è affidabile.
  • Attenzione agli argomenti mutabili: liste, dizionari e insiemi non sono hashabili e non possono essere usati direttamente come chiavi di cache. Converti eventualmente i dati in tuple o altri tipi immutabili.
  • Valuta maxsize: se il numero di combinazioni possibili è molto alto, una cache illimitata può consumare troppa memoria. In questi casi scegli un limite ragionevole.
  • Non usarla per mascherare algoritmi inefficienti: la cache aiuta, ma non sostituisce un buon algoritmo di base.
  • Controlla cache_info(): è utile per capire se la cache sta davvero portando benefici.
  • Svuota la cache quando i dati cambiano: se la funzione dipende indirettamente da parametri esterni non inclusi negli argomenti, il risultato memorizzato può diventare obsoleto.

Un altro consiglio pratico: evita di decorare funzioni che producono effetti collaterali, come scrivere su file, inviare richieste di rete o modificare stato globale. In quel caso, il riuso del risultato non è semanticamente corretto.

Esempio di uso corretto e scorretto

Uso corretto: una funzione che calcola il prezzo finale di un prodotto dato uno sconto e una tassa, sempre sugli stessi input.

Uso scorretto: una funzione che legge il prezzo da un servizio esterno in tempo reale. In questo caso, la cache potrebbe restituire dati vecchi.

Riepilogo

functools.lru_cache è uno strumento molto utile per portare nella pratica alcuni principi della programmazione funzionale in Python. È particolarmente efficace quando lavori con funzioni pure, deterministiche e costose da calcolare.

In sintesi:

  • riduce i calcoli ripetuti;
  • migliora le prestazioni in modo semplice;
  • si integra bene con funzioni ricorsive e trasformazioni pure;
  • richiede attenzione quando i dati cambiano o non sono immutabili.

Se vuoi scrivere codice più elegante e veloce, lru_cache è una delle prime tecniche da imparare dopo i concetti base della programmazione funzionale. È facile da applicare, ma molto potente quando usata nel contesto giusto.

Approfondisci con risorse ufficiali

SHARE