Unpacking e assegnazione
Esplora le potenzialità dell'unpacking e dell'assegnazione multipla in Python: dallo scambio immediato di variabili all'uso avanzato degli operatori * e ** in cicli, collezioni e funzioni (*args e **kwargs).
In Python, l'unpacking tramite assegnazione multipla consente di estrarre elementi da iterabili direttamente in variabili, migliorando leggibilità e riducendo verbosità. Questo meccanismo, che richiede una corrispondenza esatta tra variabili ed elementi (a meno dell'operatore `*`), è utile anche per scambi di valori concisi e per iterazioni strutturate, come con `dict.items()` o `enumerate()`.
Introduzione
In Python, l'unpacking (spacchettamento) è un meccanismo potente che consente di estrarre gli elementi contenuti in una struttura dati, come una lista, una tupla o un dizionario, e di assegnarli direttamente a una o più variabili in modo contestuale e leggibile.
Questa funzionalità si basa sull'iterazione interna degli oggetti. Quando si esegue un'assegnazione multipla, Python itera sulla collezione a destra dell'uguale e distribuisce i valori alle variabili specificate a sinistra, seguendo l'ordine di iterazione.
Assegnazione multipla, le basi
L'assegnazione multipla (multiple assignment) permette di assegnare più variabili in una sola istruzione, separando i nomi delle variabili con una virgola. Python esegue automaticamente l'unpacking della struttura dati a destra dell'operatore di assegnazione.
primo, secondo, terzo = (1, 2, 3)
print(primo)
print(secondo)
print(terzo)
Lo stesso comportamento funziona con qualsiasi iterabile, incluse le liste.
a, b, c = ["rosso", "verde", "blu"]
Vantaggio chiave: questo approccio riduce la verbosità del codice e migliora la leggibilità, specialmente quando si lavora con funzioni che restituiscono tuple o dati strutturati.
Cosa succede quando il numero di elementi non corrisponde
Un errore comune per chi inizia a lavorare con l'unpacking è dimenticare che il numero di variabili a sinistra deve corrispondere esattamente al numero di elementi dell'iterabile a destra, a meno di non usare l'operatore * descritto più avanti. Se i conteggi non coincidono, Python solleva un'eccezione.
a, b = [1, 2, 3]
Questo codice genera un errore di tipo ValueError, poiché la lista contiene tre elementi ma sono state fornite solo due variabili di destinazione. Conoscere questo comportamento aiuta a scrivere codice più robusto, evitando assunzioni implicite sulla forma dei dati in ingresso.
Scambio di valori senza variabile temporanea
Uno degli utilizzi più eleganti dell'assegnazione multipla è lo scambio di valori tra due variabili senza la necessità di una variabile temporanea.
a = 10
b = 20
a, b = b, a
print(a)
print(b)
Python valuta prima l'intero lato destro dell'espressione, dove b, a crea una tupla temporanea (20, 10), e poi esegue l'unpacking a sinistra. Questo elimina il rischio di errori legati all'uso di variabili temporanee e rende il codice più conciso.
Unpacking nei cicli for
L'unpacking è estremamente utile quando si iterano su strutture dati composte, come liste di tuple o dizionari.
Uso con dizionari
Il metodo items() dei dizionari restituisce un oggetto vista contenente coppie chiave-valore sotto forma di tuple. L'unpacking rende l'iterazione molto chiara.
persona = {"nome": "Anna", "eta": 25}
for chiave, valore in persona.items():
print(f"{chiave}: {valore}")
Uso con enumerate()
La funzione enumerate() è un altro caso classico: restituisce una sequenza di tuple contenenti l'indice e l'elemento corrispondente.
nomi = ["Anna", "Luca", "Marco"]
for indice, nome in enumerate(nomi):
print(f"{indice}: {nome}")
Uso con zip()
Un pattern altrettanto comune consiste nel combinare zip() con l'unpacking per iterare simultaneamente su più sequenze parallele, ad esempio due liste che rappresentano rispettivamente chiavi e valori raccolti da fonti diverse.
nomi = ["Anna", "Luca", "Marco"]
ruoli = ["Developer", "Tester", "Architect"]
for nome, ruolo in zip(nomi, ruoli):
print(f"{nome} lavora come {ruolo}")
L'operatore * nell'unpacking, raccogliere e ignorare
L'operatore * (star) ha un ruolo speciale durante l'unpacking, detto anche unpacking esteso: può essere utilizzato per raccogliere più elementi rimanenti in un'unica variabile sotto forma di lista.
numeri = [1, 2, 3, 4, 5]
primo, secondo, *resto = numeri
print(primo)
print(secondo)
print(resto)
È possibile utilizzare questa tecnica anche per ignorare elementi che non interessano, usando convenzionalmente il nome _ per la variabile di scarto.
primo, *_, ultimo = numeri
print(primo)
print(ultimo)
L'operatore * per l'espansione di collezioni
L'operatore * può essere utilizzato anche per spacchettare il contenuto di liste o tuple all'interno di una nuova collezione. Questa operazione è nota come unpacking in fase di costruzione.
a = [1, 2, 3]
b = [4, 5, 6]
unione = [*a, *b]
print(unione)
Lo stesso principio vale per le tuple.
t1 = (1, 2)
t2 = (3, 4)
t3 = (*t1, *t2)
print(t3)
L'operatore ** per i dizionari
L'operatore ** (doppio star) svolge un ruolo analogo per i dizionari: permette di espandere le coppie chiave-valore all'interno di un nuovo dizionario. È particolarmente utile per unire dizionari o per creare copie con valori sovrascritti.
dati_personali = {"nome": "Anna"}
dati_lavoro = {"ruolo": "Developer", "azienda": "TechCorp"}
profilo = {**dati_personali, **dati_lavoro}
print(profilo)
Packing, il ruolo opposto di *
Quando l'operatore * viene utilizzato in un contesto di assegnazione a sinistra, esegue il packing (impacchettamento). Il packing raccoglie più valori in un'unica lista.
primo, *altri = [10, 20, 30, 40]
print(altri)
Unpacking e packing nelle funzioni, args e kwargs
L'unpacking e il packing sono pilastri fondamentali nella definizione e chiamata delle funzioni in Python, consentendo una gestione flessibile degli argomenti.
Raccogliere argomenti posizionali con args
Quando una funzione deve accettare un numero arbitrario di argomenti posizionali, si utilizza *args nella firma. Python raccoglie tutti gli argomenti posizionali extra in una tupla.
def somma(*numeri):
return sum(numeri)
print(somma(1, 2, 3, 4))
Raccogliere argomenti nominati con kwargs
Analogamente, **kwargs raccoglie tutti gli argomenti passati con nome (keyword arguments) in un dizionario.
def stampa_info(**dati):
for chiave, valore in dati.items():
print(f"{chiave}: {valore}")
stampa_info(nome="Luca", eta=30)
Passare collezioni come argomenti, unpacking in chiamata
Durante la chiamata di una funzione, è possibile fare l'operazione inversa: spacchettare una lista o una tupla in argomenti posizionali con *, o un dizionario in argomenti nominati con **. Questo rende il codice molto flessibile e riduce la necessità di wrapper.
valori = (1, 2, 3)
somma(*valori)
dati = {"nome": "Anna", "eta": 25}
stampa_info(**dati)
Forzare argomenti keyword-only con l'asterisco isolato
Un pattern legato all'unpacking, ma spesso sottovalutato, consiste nell'uso di un asterisco isolato nella firma di una funzione per forzare gli argomenti successivi a essere passati esclusivamente per nome. Questo migliora la leggibilità delle chiamate e riduce gli errori legati all'ordine dei parametri, un aspetto rilevante per la manutenibilità del codice in team.
def crea_utente(nome, eta, *, ruolo="guest"):
return {"nome": nome, "eta": eta, "ruolo": ruolo}
crea_utente("Anna", 25, ruolo="admin")
In questo esempio, ruolo può essere fornito soltanto specificandone esplicitamente il nome, non come terzo argomento posizionale.
Best practice e pattern avanzati
Pattern matching strutturale
Le versioni moderne di Python integrano l'unpacking nei costrutti di pattern matching, consentendo estrazioni complesse in modo dichiarativo.
dati = ["utente", {"nome": "Marco", "ruolo": "admin"}]
match dati:
case ["utente", {"nome": nome, "ruolo": ruolo}]:
print(f"Utente {nome} con ruolo {ruolo}")
case _:
print("Formato dati non riconosciuto")
Unpacking con NamedTuple
L'unpacking si applica in modo naturale anche alle namedtuple, una struttura dati molto usata per modellare record leggibili e immutabili, tema rilevante per chi punta a una chiarezza architetturale superiore rispetto a semplici tuple anonime.
from collections import namedtuple
Punto = namedtuple("Punto", ["x", "y"])
p = Punto(3, 4)
x, y = p
print(x)
print(y)
Design pattern e Python
L'unpacking e l'espansione di collezioni sono alla base di numerosi design pattern:
- Factory Pattern: utilizzare l'unpacking per disaccoppiare la creazione di oggetti.
- Decorator Pattern:
*argse**kwargspermettono di scrivere decoratori generici che preservano la firma delle funzioni originali. - Adapter Pattern: l'uso di
**per adattare dizionari di configurazione a firme di funzioni è una pratica comune per rendere le interfacce più flessibili.
Una nota sulle prestazioni
Dal punto di vista dell'efficienza del codice, l'unpacking non introduce un overhead significativo rispetto all'accesso per indice: il costo computazionale resta paragonabile, mentre il guadagno in leggibilità e in riduzione della probabilità di errori umani è concreto. Per questo motivo l'unpacking è generalmente preferibile quando la struttura dei dati è nota e stabile.
Attenzione, usi distinti di * e **
È fondamentale non confondere i diversi usi di * e **, che ricorrono in due contesti completamente separati del linguaggio:
- Operatori matematici:
2 * 3per la moltiplicazione,2 ** 3per l'elevamento a potenza. - Operatori di unpacking e packing:
*valorie**datiin contesti di assegnazione, definizione o chiamata di funzione.
Comprendere questa differenza è essenziale per sfruttare appieno una delle caratteristiche più potenti e caratteristiche dello stile Python, evitando al contempo ambiguità di lettura nel codice.
Riepilogo delle sintassi principali
| Contesto | Sintassi | Comportamento |
|---|---|---|
| Assegnazione multipla | a, b = x, y |
Unpacking di iterabili |
| Unpacking esteso | a, *b = [1,2,3] |
Packing degli elementi rimanenti in b |
| Costruzione di collezioni | [*a, *b] |
Espansione di iterabili in nuove liste |
| Definizione funzione | def f(*args, **kwargs) |
Packing di argomenti posizionali e nominati |
| Chiamata funzione | f(*lista, **dict) |
Unpacking di iterabili e dizionari in argomenti |
| Argomenti keyword-only | def f(a, b, *, c) |
Forza c a essere passato solo per nome |