Introduzione
I dizionari sono una delle strutture dati più potenti di Python perché permettono di associare una chiave a un valore in modo rapido e intuitivo. Un uso molto pratico, spesso sottovalutato, è la costruzione di una cache: un meccanismo che memorizza risultati già calcolati per evitare di ripetere operazioni costose.
Questo approccio è utile in molti scenari reali: elaborazione di dati, chiamate a funzioni lente, conversioni ripetute, calcoli matematici, accesso a risorse esterne e persino simulazioni. Invece di ricalcolare ogni volta lo stesso risultato, salviamo il valore in un dizionario e lo riutilizziamo quando serve.
In questo tutorial vedremo come implementare una cache semplice ma concreta in Python, usando i dizionari in modo corretto e leggibile. L’obiettivo non è solo capire la sintassi, ma anche ragionare su quando questa tecnica migliora davvero il codice.
Codice completo
class CacheCalcoli:
def __init__(self):
# Dizionario interno che conserva i risultati già calcolati
self._cache = {}
def fattoriale(self, n):
"""
Restituisce il fattoriale di n.
Se il risultato è già presente nella cache, lo riusa.
"""
if n < 0:
raise ValueError("Il fattoriale non è definito per numeri negativi")
# La chiave della cache è il numero n
if n in self._cache:
print(f"[CACHE HIT] Uso il risultato già salvato per n={n}")
return self._cache[n]
print(f"[CACHE MISS] Calcolo il fattoriale di n={n}")
risultato = 1
for i in range(2, n + 1):
risultato *= i
# Salvataggio del risultato nella cache
self._cache[n] = risultato
return risultato
def fib(self, n):
"""
Restituisce il numero di Fibonacci in posizione n.
Anche qui usiamo la cache per evitare ricalcoli.
"""
if n < 0:
raise ValueError("Fibonacci non è definito per numeri negativi")
if n in self._cache:
print(f"[CACHE HIT] Uso il risultato già salvato per n={n}")
return self._cache[n]
print(f"[CACHE MISS] Calcolo Fibonacci di n={n}")
if n == 0:
risultato = 0
elif n == 1:
risultato = 1
else:
# Ricorsione semplice, resa più efficiente dalla cache
risultato = self.fib(n - 1) + self.fib(n - 2)
self._cache[n] = risultato
return risultato
def stato_cache(self):
"""Mostra il contenuto della cache in modo leggibile."""
return dict(self._cache)
# Esempio di utilizzo
cache = CacheCalcoli()
print("Fattoriale di 5:", cache.fattoriale(5))
print("Fattoriale di 5 di nuovo:", cache.fattoriale(5))
print("Fibonacci di 8:", cache.fib(8))
print("Fibonacci di 8 di nuovo:", cache.fib(8))
print("Contenuto cache:", cache.stato_cache()) Spiegazione
Il cuore dell’esempio è il dizionario _cache, che associa una chiave a un risultato già calcolato. In questo caso la chiave è il valore di input della funzione, ad esempio n. Se chiediamo più volte il fattoriale di 5, il calcolo viene eseguito una sola volta: al secondo accesso il programma recupera il dato dal dizionario.
1. La logica base della cache
Il flusso è molto semplice:
- controlliamo se la chiave è già presente nel dizionario;
- se sì, restituiamo il valore memorizzato;
- se no, calcoliamo il risultato;
- salviamo il risultato nella cache;
- restituiamo il valore.
Questa tecnica è utile perché l’accesso a un dizionario in Python è mediamente molto veloce. In molti casi, il costo di cercare un valore già salvato è molto più basso rispetto al ricalcolo completo.
2. Perché usare i dizionari per una cache
Un dizionario è perfetto per questo compito perché funziona come una tabella di associazione. La chiave rappresenta l’input, mentre il valore rappresenta il risultato. Ad esempio:
- chiave 5 → valore 120 per il fattoriale;
- chiave 8 → valore 21 per Fibonacci.
Questo modello è molto naturale. Ogni volta che la funzione riceve lo stesso input, il risultato è identico, quindi non ha senso ricalcolarlo.
3. Il caso del fattoriale
Nel metodo fattoriale il calcolo è iterativo. È un buon esempio perché mostra una situazione in cui la cache può evitare ripetizioni inutili, soprattutto se la stessa funzione viene chiamata molte volte con gli stessi parametri.
4. Il caso di Fibonacci
Il metodo fib è ancora più interessante. La versione ricorsiva classica di Fibonacci è notoriamente inefficiente, perché ricalcola gli stessi sotto-problemi molte volte. Qui la cache riduce drasticamente il numero di chiamate ripetute.
Per esempio, calcolare Fibonacci di 8 senza cache genera molte ricorsioni ridondanti. Con il dizionario, invece, ogni valore viene memorizzato una volta sola e riutilizzato in seguito.
5. Il metodo stato_cache
Il metodo stato_cache restituisce una copia del contenuto del dizionario. Convertire self._cache in un nuovo dizionario con dict() è una scelta utile per mostrare i dati senza esporre direttamente la struttura interna.
Best practice
- Usa chiavi immutabili: nei dizionari le chiavi devono essere hashable. Numeri, stringhe e tuple sono ottime scelte.
- Separa i dati interni: usa un attributo come _cache per indicare che il dizionario è interno alla classe.
- Gestisci gli errori: controlla input non validi, come numeri negativi o valori fuori dominio.
- Non abusare della cache: memorizzare tutto può occupare memoria inutilmente. La cache è utile solo se i risultati vengono davvero riutilizzati.
- Valuta la coerenza dei dati: se il risultato dipende da fattori esterni, una cache semplice potrebbe diventare obsoleta.
- Usa nomi chiari: chiamare la variabile cache o _cache rende immediata la comprensione del codice.
Quando la cache è davvero utile
La cache dà il meglio di sé quando una funzione è:
- costosa da eseguire;
- chiamata più volte con gli stessi parametri;
- deterministica, cioè produce sempre lo stesso output per lo stesso input.
Se invece il risultato cambia spesso o dipende da dati esterni in continuo aggiornamento, la cache può diventare un problema invece che un vantaggio.
Estensione pratica: chiavi composte
In scenari più avanzati, la chiave del dizionario può essere una tupla che rappresenta più parametri. Ad esempio, una funzione che calcola uno sconto potrebbe usare come chiave (prezzo, percentuale, categoria). Questo permette di memorizzare risultati più complessi senza perdere precisione.
# Esempio di chiave composta
cache_sconti = {}
chiave = (100, 20, "cliente_premium")
cache_sconti[chiave] = 80 Questo pattern è molto utile quando il risultato dipende da più input contemporaneamente.
Riepilogo
I dizionari in Python non servono solo a rappresentare dati come anagrafica, configurazioni o associazioni chiave-valore. Possono diventare uno strumento strategico per migliorare le prestazioni del codice attraverso una cache semplice e leggibile.
In questo tutorial abbiamo visto come:
- usare un dizionario per memorizzare risultati già calcolati;
- evitare ricalcoli inutili;
- applicare la tecnica a fattoriale e Fibonacci;
- gestire chiavi semplici e chiavi composte;
- seguire buone pratiche per mantenere il codice pulito e sicuro.
Se vuoi scrivere programmi più efficienti, imparare a usare i dizionari come cache è un passo molto utile. È una tecnica semplice, ma con un impatto concreto sulla qualità del software.
Approfondisci con risorse ufficiali
- Documentazione ufficiale Python sui dizionari: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
- Funzioni built-in di Python: https://docs.python.org/3/library/functions.html
- Data model di Python e concetto di hashability: https://docs.python.org/3/reference/datamodel.html
- Modulo functools, utile per memoization: https://docs.python.org/3/library/functools.html
Se vuoi fare un passo in più, prova a confrontare questa implementazione manuale con @functools.lru_cache: è un ottimo esercizio per capire come Python può automatizzare la memoization in modo elegante.
