W3docs

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.SimpleNamespace per l'accesso tramite punto
  • Usare collections.namedtuple per 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 attribute

Se 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)      # 31

Più 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 Bob

Quando 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.com

Non è 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

StrumentoMutabileAccesso a puntoMetodiType hintIdeale per
dictNo (["key"])NoNoCampi dinamici / sconosciuti
SimpleNamespaceNoNoConfig ad hoc, fixture di test
namedtupleNoNoParzialeRecord immutabili, dati piccoli
classTramite annotazioniOOP con comportamento
@dataclassSì*Record strutturati con metodi

*frozen=True rende una dataclass immutabile.

Regola generale:

  • Usa un dict quando la struttura non è nota in anticipo.
  • Usa SimpleNamespace quando vuoi l'accesso a punto senza una definizione di classe.
  • Usa namedtuple per record semplici e immutabili (coordinate, colori, righe).
  • Usa una class normale quando hai bisogno di metodi e OOP completo.
  • Usa @dataclass quando hai bisogno di un record strutturato con metodi opzionali — offre il massimo con il minimo codice ripetitivo.

Argomenti correlati

Esercitazione

Pratica
In Python, what are the main reasons for grouping variables into classes?
In Python, what are the main reasons for grouping variables into classes?
Was this page helpful?