Raggruppamento di Variabili in Python
Impara a raggruppare variabili correlate in Python con classi, dataclass, named tuple, SimpleNamespace e dizionari — con esempi eseguibili.
Quando un programma deve tenere traccia di più dati correlati — il nome, l'età e l'email di un utente, ad esempio — conservarli in variabili separate e non collegate diventa difficile da gestire. Python offre diversi strumenti per raggruppare variabili sotto un unico nome, in modo che viaggino insieme e rimangano organizzate. Questo capitolo spiega gli approcci più comuni, quando usare ciascuno e i relativi compromessi.
Argomenti trattati:
- Perché raggruppare le variabili è importante
- Usare un dizionario semplice
- Usare
types.SimpleNamespaceper l'accesso tramite punto - Usare
collections.namedtupleper record immutabili leggeri - Usare una classe con
__init__ - Usare
@dataclass(Python 3.7+) per la sintassi più pulita - Scegliere lo strumento giusto
Perché raggruppare le variabili?
Supponiamo di scrivere uno script che elabora account utente. Senza raggruppamento si potrebbe scrivere:
user_name = "Alice"
user_age = 30
user_email = "[email protected]"Questo funziona per un singolo utente, ma si complica nel momento in cui si ha bisogno di due utenti o di passare dati a una funzione:
def greet(name, age, email):
print(f"Hello {name}, age {age} ({email})")
greet(user_name, user_age, user_email)Tre argomenti separati devono rimanere sincronizzati ovunque. Il raggruppamento risolve questo problema aggregando i dati:
user = {"name": "Alice", "age": 30, "email": "[email protected]"}
def greet(user):
print(f"Hello {user['name']}, age {user['age']} ({user['email']})")
greet(user)Ora la firma della funzione ha un solo parametro invece di tre, e aggiungere un nuovo campo tocca soltanto il dizionario.
Usare un dizionario
Un dizionario Python è il modo più semplice per raggruppare variabili con nome. Le chiavi sono string; i valori possono essere di qualsiasi tipo.
point = {"x": 10, "y": 20, "label": "origin"}
print(point["x"]) # 10
print(point["label"]) # origin
# Update a field
point["x"] = 15
print(point)
# {'x': 15, 'y': 20, 'label': 'origin'}Quando usarlo: raggruppamento rapido e occasionale, dati JSON, situazioni in cui l'insieme dei campi non è definito in anticipo.
Svantaggi: si accede ai campi tramite chiavi string (point["x"]), che è più prolisso della notazione a punto e non offre completamento automatico nell'IDE.
Usare types.SimpleNamespace
SimpleNamespace è un sottile wrapper che offre l'accesso tramite punto su un namespace ad hoc senza dover scrivere una classe.
from types import SimpleNamespace
point = SimpleNamespace(x=10, y=20, label="origin")
print(point.x) # 10
print(point.label) # origin
# Update a field
point.x = 15
print(point)
# namespace(x=15, y=20, label='origin')Gli object SimpleNamespace sono mutabili — è possibile aggiungere, modificare o eliminare attributi in qualsiasi momento:
from types import SimpleNamespace
config = SimpleNamespace(debug=False, timeout=30)
config.debug = True # update
config.retries = 3 # add new attribute
del config.timeout # remove
print(vars(config))
# {'debug': True, 'retries': 3}Quando usarlo: per sostituire un dizionario quando si vuole l'accesso tramite punto senza bisogno di metodi o controllo dei tipi. Ottimo per fixture di test e oggetti di configurazione semplici.
Usare collections.namedtuple
Un namedtuple è un record immutabile e leggero. Si comporta come una tupla normale ma consente di accedere ai campi per nome oltre che per indice.
from collections import namedtuple
# Define the type once
Point = namedtuple("Point", ["x", "y"])
# Create an instance
p = Point(x=10, y=20)
print(p.x) # 10
print(p.y) # 20
print(p[0]) # 10 — index access still works
print(p) # Point(x=10, y=20)Poiché le istanze di namedtuple sono immutabili, non è possibile modificare un campo dopo la creazione:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
# white.red = 0 # AttributeError: can't set attributeSe occorre una copia modificata, si usa il metodo _replace() — restituisce una nuova istanza:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
grey = white._replace(red=128, green=128, blue=128)
print(grey)
# Color(red=128, green=128, blue=128)Quando usarlo: record immutabili in cui i nomi dei campi sono importanti — coordinate, colori RGB, righe di database. Occupa meno memoria rispetto a una classe completa.
Usare una classe
Per variabili raggruppate che necessitano anche di comportamento (metodi), si definisce una classe con un metodo __init__:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def greet(self):
return f"Hello, I am {self.name} and I am {self.age} years old."
alice = User("Alice", 30, "[email protected]")
print(alice.name) # Alice
print(alice.greet()) # Hello, I am Alice and I am 30 years old.
# Update a field
alice.age = 31
print(alice.age) # 31Più istanze rimangono indipendenti — ognuna mantiene la propria copia di name, age e email:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
alice = User("Alice", 30, "[email protected]")
bob = User("Bob", 25, "[email protected]")
print(alice.name, bob.name) # Alice BobQuando usarlo: ogni volta che i dati raggruppati necessitano anche di metodi, logica di validazione o ereditarietà. Le classi sono il fondamento della programmazione orientata agli oggetti in Python — vedi Python Classes and Objects per una spiegazione completa.
Usare @dataclass (Python 3.7+)
Il decoratore @dataclass genera automaticamente __init__, __repr__ e __eq__ dai campi della classe annotati, eliminando la maggior parte del codice ripetitivo:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
label: str = "unnamed"
p = Point(x=3.0, y=4.0)
print(p) # Point(x=3.0, y=4.0, label='unnamed')
print(p.label) # unnamed
p.label = "A"
print(p) # Point(x=3.0, y=4.0, label='A')I campi con un valore predefinito devono venire dopo i campi senza (stessa regola degli argomenti di funzione normali).
Dataclass immutabile con frozen=True
Passare frozen=True impedisce la modifica di qualsiasi campo dopo la creazione — comportamento simile a un namedtuple ma con tutte le capacità di una classe:
from dataclasses import dataclass
@dataclass(frozen=True)
class RGB:
red: int
green: int
blue: int
white = RGB(255, 255, 255)
print(white)
# RGB(red=255, green=255, blue=255)
# white.red = 0 # FrozenInstanceError: cannot assign to field 'red'Raggruppare più record in una lista
Le dataclass funzionano naturalmente con le liste quando si ha bisogno di una collezione di record:
from dataclasses import dataclass
from typing import List
@dataclass
class Product:
name: str
price: float
in_stock: bool = True
inventory: List[Product] = [
Product("Widget", 9.99),
Product("Gadget", 24.99),
Product("Doohickey", 4.50, in_stock=False),
]
for item in inventory:
status = "available" if item.in_stock else "out of stock"
print(f"{item.name}: ${item.price:.2f} ({status})")Output:
Widget: $9.99 (available)
Gadget: $24.99 (available)
Doohickey: $4.50 (out of stock)Per il set completo di funzionalità delle dataclass, inclusi field(), __post_init__ e l'ereditarietà, vedi Python Dataclasses.
Raggruppare variabili con attributi di classe
A volte si vogliono costanti condivise associate a un gruppo anziché dati per istanza. Gli attributi di classe (definiti direttamente nel corpo della classe, al di fuori di __init__) sono condivisi tra tutte le istanze:
class AppConfig:
MAX_RETRIES = 3
TIMEOUT = 30
BASE_URL = "https://api.example.com"
print(AppConfig.MAX_RETRIES) # 3
print(AppConfig.BASE_URL) # https://api.example.comNon è necessario istanziare AppConfig per leggerne gli attributi — si tratta la classe stessa come un namespace per costanti correlate. È un pattern leggero per gruppi di configurazione. Per una discussione più approfondita sugli attributi di classe rispetto a quelli di istanza, vedi Python Classes and Objects.
Scegliere lo strumento giusto
| Strumento | Mutabile | Accesso a punto | Metodi | Type hint | Ideale per |
|---|---|---|---|---|---|
dict | Sì | No (["key"]) | No | No | Campi dinamici / sconosciuti |
SimpleNamespace | Sì | Sì | No | No | Config ad hoc, fixture di test |
namedtuple | No | Sì | No | Parziale | Record immutabili, dati piccoli |
class | Sì | Sì | Sì | Tramite annotazioni | OOP con comportamento |
@dataclass | Sì* | Sì | Sì | Sì | Record strutturati con metodi |
*frozen=True rende una dataclass immutabile.
Regola generale:
- Usa un
dictquando la struttura non è nota in anticipo. - Usa
SimpleNamespacequando vuoi l'accesso a punto senza una definizione di classe. - Usa
namedtupleper record semplici e immutabili (coordinate, colori, righe). - Usa una
classnormale quando hai bisogno di metodi e OOP completo. - Usa
@dataclassquando hai bisogno di un record strutturato con metodi opzionali — offre il massimo con il minimo codice ripetitivo.
Argomenti correlati
- Python Variables — come funzionano le variabili e le regole di denominazione
- Variable Names — convenzioni di denominazione e best practice
- Global Variables — variabili a livello di modulo e la parola chiave
global - Python Classes and Objects — spiegazione completa dell'OOP
- Python Dataclasses — approfondimento su
@dataclass - Assign Multiple Values — unpacking e assegnazione multipla