Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Tuples and Frozensets: Immutable Data Structures

Recommended resources

Ciao e bentornato!

Introduzione

Nella lezione precedente abbiamo esplorato il "perché" dell'immutabilità, scoprendo come essa migliori la correttezza del programma e semplifichi drasticamente la gestione della concorrenza. Abbiamo capito che evitare mutazioni nascoste rende il codice più prevedibile e robusto.

Oggi passiamo dalla teoria alla pratica. L'obiettivo di questa lezione è imparare a usare le strutture dati immutabili native di Python, tuple e frozenset. Vedremo come queste ci permettono di applicare i concetti di immutabilità che abbiamo appena appreso, scrivendo codice che è immutabile by design. Inizieremo a costruire le fondamenta pratiche del nostro stile di programmazione funzionale.


1. Il tuple: una sequenza immutabile

La prima e più comune struttura dati immutabile che incontreremo è la tuple. Puoi pensarla come la controparte immutabile della list. Come una lista, una tupla è una sequenza ordinata di elementi a cui si può accedere tramite un indice. La differenza cruciale è che, una volta creata, non può essere modificata.

Python's Mutable vs Immutable Types: What's the Difference?

Per una definizione formale e alcuni esempi di base, leggi la sezione dedicata alle tuple nell'articolo di Real Python.

Leggi la sezione intitolata "Tuples". Concentrati su come si creano le tuple e sull'errore (TypeError) che si ottiene quando si tenta di modificare un elemento.

Questo sembra semplice, ma c'è una sottigliezza fondamentale che è cruciale da capire per un programmatore esperto. L'immutabilità di una tupla è "superficiale" (shallow). La tupla stessa non può essere modificata, ma cosa succede se uno dei suoi elementi è un oggetto mutabile, come una lista?

Il seguente video spiega questo "paradosso" in modo eccezionalmente chiaro.

Immutable vs Mutable Objects in Python

Questo video di Real Python mostra una situazione che spesso confonde i neofiti di Python: una tupla immutabile che contiene una lista mutabile.

Guarda la sezione dal timestamp 05:31 al 09:19. Presta molta attenzione a come è possibile modificare la lista contenuta nella tupla e perché, dal punto di vista di Python, questo non viola l'immutabilità della tupla stessa.

Questo concetto di immutabilità non transitiva è vitale. Significa che per garantire una vera immutabilità, non solo il contenitore (la tupla) deve essere immutabile, ma anche tutti i suoi contenuti devono esserlo.

Costruire una struttura dati veramente immutabile

Come possiamo applicare questo principio in un caso reale? Immagina di avere una lista di dizionari, una struttura dati molto comune in Python. Sia le liste che i dizionari sono mutabili, rendendo l'intera struttura vulnerabile a modifiche accidentali.

Il video seguente mostra come trasformare questa struttura dati mutabile in una versione completamente immutabile, combinando tuple con un'altra utile utility, collections.namedtuple.

Functional Programming in Python: Immutable Data Structures

Vediamo ora un esempio pratico di refactoring verso l'immutabilità. Questo video di Real Python trasforma una lista di dizionari in una tupla di namedtuple.

Guarda il video dal timestamp 05:30 al 16:45. Segui questi passaggi: Il problema (05:30 - 09:47): Osserva come la struttura list[dict] sia mutabile e perché questo sia un problema nello stile funzionale. Una soluzione parziale (10:41 - 13:33): Vedi come collections.namedtuple viene usato per creare record immutabili per i singoli scienziati, risolvendo il problema della mutabilità dei dizionari. La soluzione completa (13:33 - 16:45): Infine, osserva come questi namedtuple immutabili vengano inseriti in una tuple, creando una collezione esterna anch'essa immutabile. Il risultato finale è una tuple[Scientist], una struttura dati robusta e completamente immutabile.

Questo esempio è un ottimo modello di come si pensa in modo funzionale: invece di usare strutture generiche e mutabili, si definiscono tipi di dati specifici e immutabili che modellano il dominio del problema in modo più sicuro e chiaro.


2. Il frozenset: un insieme immutabile

Proprio come la tupla è la versione immutabile della lista, Python fornisce il frozenset come controparte immutabile del set.

Set vs. FrozenSet in Python
Questa immagine mostra la sintassi per creare un `set` mutabile e un `frozenset` immutabile, evidenziando la loro principale differenza.

Un frozenset è una collezione non ordinata di elementi unici che, una volta creata, non può essere alterata. Non puoi aggiungere o rimuovere elementi. Allora perché usarlo?

La sua immutabilità gli conferisce una proprietà fondamentale che un set normale non ha: è hashable. Questo significa che può essere usato in contesti dove è richiesto un valore immutabile e calcolabile, come la chiave di un dizionario o un elemento all'interno di un altro set.

L'articolo seguente di DataCamp fornisce un'eccellente panoramica del frozenset.

Python Frozenset: Why It Exists and When You Need It

Questo articolo spiega in dettaglio cos'è un frozenset, perché esiste e quando dovresti usarlo.

Leggi le seguenti sezioni per ottenere una comprensione completa: What Is a frozenset in Python?: Una definizione di base. Frozenset vs. Other Python Collections: Un confronto chiave con set e tuple che chiarisce quando usare l'uno o l'altro. Python Frozenset and Hashability: La sezione più importante, che spiega perché l'immutabilità porta all'hashability e mostra l'esempio pratico dell'uso di frozenset come chiave di un dizionario. Common Use Cases for frozenset: Esempi pratici come la rappresentazione di permessi o configurazioni fisse.

Esempio pratico: set vs frozenset come chiave

Proviamo a vedere il concetto di hashability in azione. Esegui questo codice nella tua console Python:




# Un set mutabile NON può essere una chiave di dizionario
permissions_set = {"read", "write"}
user_permissions = {permissions_set: "Admin"}



# Output: TypeError: unhashable type: 'set'




# Un frozenset immutabile PUÃ’ essere una chiave di dizionario
permissions_frozenset = frozenset({"read", "write"})
user_permissions = {permissions_frozenset: "Admin"}
print(user_permissions)



# Output: {frozenset({'write', 'read'}): 'Admin'}

Questo dimostra il principale vantaggio pratico del frozenset: permette di usare una collezione di tipo "set" in contesti che richiedono immutabilità.


Esercizio di riflessione

Immagina di dover modellare i dati di un prodotto per un sito di e-commerce. Un prodotto ha le seguenti informazioni:

  • Un product_id univoco (es. 12345).
  • Un name (es. "Tastiera Meccanica").
  • Una lista di category_tags (es. ["elettronica", "periferiche", "gaming"]). Queste categorie sono fisse per il prodotto.
  • Una cronologia dei price_changes, che è una lista di tuple (timestamp, prezzo). Questa cronologia cresce nel tempo.

Rifletti su quali strutture dati (mutabili o immutabili) useresti per rappresentare un singolo prodotto e le sue proprietà. Non c'è un'unica risposta giusta, ma giustifica le tue scelte in base ai principi di immutabilità e all'intento che vuoi comunicare con il tuo codice.

Clicca per vedere una possibile soluzione

Una possibile modellazione potrebbe essere questa:

  • product_id e name: Essendo int e str, sono già immutabili.
  • category_tags: Poiché le categorie sono fisse e il loro ordine non è importante, un frozenset è una scelta eccellente. Comunica chiaramente l'intento che questi tag sono un gruppo immutabile e unico di proprietà.
  • price_changes: Poiché la cronologia cresce nel tempo, una list mutabile è appropriata. Tuttavia, gli elementi al suo interno ((timestamp, prezzo)) dovrebbero essere tuple, poiché ogni singola voce di prezzo non dovrebbe cambiare.

Potresti ulteriormente incapsulare le proprietà immutabili in un namedtuple per creare un record di prodotto chiaro e sicuro:

from collections import namedtuple

Product = namedtuple("Product", ["product_id", "name", "category_tags"])




# Dati di esempio
product_record = Product(
    product_id=12345,
    name="Tastiera Meccanica",
    category_tags=frozenset(["elettronica", "periferiche", "gaming"])
)

price_history = [
    ("2023-01-01T10:00:00Z", 120.00),
    ("2023-02-15T14:30:00Z", 115.50)
]




# Aggiunta di una nuova variazione di prezzo (mutazione controllata)
price_history.append(("2023-03-01T09:00:00Z", 118.00))

print(product_record)
print(price_history)

In questo modello, i dati "statici" del prodotto sono protetti dalla mutazione, mentre i dati "dinamici" (la cronologia dei prezzi) possono essere modificati in modo esplicito e controllato.


Conclusione

In questa lezione abbiamo messo in pratica il concetto di immutabilità usando gli strumenti che Python ci offre.

Punti chiave:

  • tuple è la controparte immutabile della list, usata per sequenze ordinate e fisse.
  • frozenset è la controparte immutabile del set, usato per collezioni non ordinate e uniche, con il vantaggio di essere hashable e quindi utilizzabile come chiave di dizionario.
  • L'immutabilità in Python è "superficiale": una tupla può contenere oggetti mutabili. Per una vera immutabilità, tutti gli elementi annidati devono essere immutabili.
  • La scelta tra tuple e frozenset dipende dall'intento: tuple per la struttura posizionale, frozenset per la verifica di appartenenza.

Abbiamo capito il "perché" dell'immutabilità e ora abbiamo visto il "come" implementarla con le strutture dati di base. Il passo successivo è unire questi concetti.

Nella prossima lezione, rinnoveremo (refactoring) del codice imperativo esistente per utilizzare questi pattern di dati immutabili, trasformando funzioni con effetti collaterali in funzioni pure che operano su tuple e frozenset.