Python @staticmethod e @classmethod
Scopri come funzionano i decoratori @staticmethod e @classmethod in Python, quando usarli e come scrivere factory method e helper con esempi pratici.
Python assegna a ogni metodo all'interno di una classe uno dei tre stili di binding: può essere associato a un'istanza, alla classe stessa oppure a nessuno dei due. I decoratori @classmethod e @staticmethod controllano questi ultimi due stili.
Questo capitolo tratta:
- I tre tipi di metodo e le loro differenze
@staticmethod— una semplice funzione memorizzata all'interno di una classe@classmethod— un metodo che riceve la classe come primo argomento- Factory method: l'uso reale più comune di
@classmethod - Costruttori alternativi e come interagiscono con l'ereditarietà
- Quando scegliere
@staticmethodvs@classmethodvs una funzione a livello di modulo - Errori comuni
Prima di leggere, assicurati di avere familiarità con le classi e gli oggetti Python e con l'ereditarietà in Python. Per l'accesso agli attributi calcolati, consulta @property. Per un approfondimento su come funzionano i decoratori in generale, vedi Decoratori Python.
I Tre Tipi di Metodo
Prima di esaminare ogni decoratore, ecco un confronto affiancato:
| Metodo di istanza | @classmethod | @staticmethod | |
|---|---|---|---|
| Primo parametro | self (l'istanza) | cls (la classe) | nessuno |
| Riceve l'istanza? | Sì | No | No |
| Riceve la classe? | Tramite type(self) | Sì (direttamente) | No |
| Chiamato su un'istanza | Sì | Sì | Sì |
| Chiamato sulla classe | Sì (ma self manca) | Sì | Sì |
| Uso tipico | Operare sui dati dell'istanza | Factory method, stato a livello di classe | Funzioni di utilità/helper |
class Demo:
def instance_method(self):
return f"instance method — self is {self}"
@classmethod
def class_method(cls):
return f"class method — cls is {cls}"
@staticmethod
def static_method():
return "static method — no self, no cls"
d = Demo()
print(d.instance_method()) # instance method — self is <__main__.Demo object at 0x...>
print(d.class_method()) # class method — cls is <class '__main__.Demo'>
print(d.static_method()) # static method — no self, no cls
# All three can also be called directly on the class:
print(Demo.class_method()) # class method — cls is <class '__main__.Demo'>
print(Demo.static_method()) # static method — no self, no cls@staticmethod
Un metodo statico è il più semplice dei tre. È semplicemente una funzione normale che si trova all'interno del namespace di una classe. Python non passa automaticamente self o cls.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def is_even(n):
return n % 2 == 0
print(MathUtils.add(3, 4)) # 7
print(MathUtils.is_even(10)) # TrueQuando usare @staticmethod
Usa @staticmethod quando un helper appartiene logicamente a una classe — per chiarezza, raggruppamento o namespacing — ma non ha bisogno di leggere o modificare né lo stato dell'istanza né quello della classe:
- Helper di validazione chiamati prima di costruire un oggetto.
- Funzioni di conversione o calcolo pure che hanno senso solo nel contesto di una classe.
- Funzioni di utilità usate da diversi metodi della stessa classe ma in nessun altro posto.
class Temperature:
def __init__(self, celsius):
if not Temperature._is_valid(celsius):
raise ValueError(f"Temperature {celsius} °C is below absolute zero")
self.celsius = celsius
@staticmethod
def _is_valid(celsius):
return celsius >= -273.15
@staticmethod
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
t = Temperature(100)
print(Temperature.celsius_to_fahrenheit(100)) # 212.0
print(Temperature._is_valid(-300)) # FalseNota che _is_valid ha il prefisso _ per segnalare che è interno alla classe. I chiamanti che hanno bisogno solo di oggetti Temperature non lo vedranno mai — riceveranno semplicemente un ValueError se passano un valore impossibile.
@staticmethod vs una funzione a livello di modulo
Una funzione a livello di modulo e un @staticmethod sono quasi identici nel comportamento. La differenza è dove vive la funzione:
- Se la funzione è rilevante solo per
Temperature(o viene chiamata esclusivamente dall'interno diTemperature), inseriscila nella classe come@staticmethod. - Se è un'utilità generica usata in tutto il modulo, inseriscila a livello di modulo.
Non c'è differenza di prestazioni. Questa è puramente una scelta organizzativa.
@classmethod
Un metodo di classe riceve la classe come primo argomento (chiamato cls per convenzione — ma proprio come self, il nome è una convenzione, non una parola chiave). Poiché ha un riferimento alla classe, può:
- Leggere o modificare attributi a livello di classe.
- Creare e restituire nuove istanze della classe (factory method).
- Funzionare correttamente con le sottoclassi (factory polimorfiche).
class Counter:
_count = 0 # class-level attribute
def __init__(self):
Counter._count += 1
@classmethod
def get_count(cls):
return cls._count
@classmethod
def reset(cls):
cls._count = 0
Counter()
Counter()
Counter()
print(Counter.get_count()) # 3
Counter.reset()
print(Counter.get_count()) # 0Factory method — il caso d'uso più importante
L'uso più comune e prezioso di @classmethod è come factory method (detto anche costruttore alternativo). Un factory method crea istanze a partire da diversi tipi di input senza appesantire __init__ con logica condizionale.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
"""Create a Date from an ISO 8601 string, e.g. '2024-03-15'."""
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_tuple(cls, date_tuple):
"""Create a Date from a (year, month, day) tuple."""
return cls(*date_tuple)
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-03-15")
d3 = Date.from_tuple((2024, 3, 15))
print(d1) # Date(2024, 3, 15)
print(d2) # Date(2024, 3, 15)
print(d3) # Date(2024, 3, 15)__init__ rimane semplice — memorizza solo tre interi. I metodi di classe gestiscono la logica di conversione. Questo è più pulito di un singolo __init__ con più parametri opzionali e rami if/elif.
Perché cls è importante per l'ereditarietà
Quando un factory class method chiama cls(...) invece di codificare a parte il nome della classe, crea un'istanza di qualunque classe su cui il metodo è stato chiamato — anche una sottoclasse. Per questo dovresti sempre preferire cls(...) a ClassName(...) all'interno di un @classmethod.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"{type(self).__name__}({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day) # uses cls, not Date
class DateTime(Date):
pass # inherits from_string
dt = DateTime.from_string("2024-03-15")
print(dt) # DateTime(2024, 3, 15) — correct subclass
print(type(dt)) # <class '__main__.DateTime'>Se from_string avesse codificato a parte return Date(year, month, day), chiamare DateTime.from_string(...) restituirebbe un Date, non un DateTime — rompendo silenziosamente il contratto di ereditarietà.
Modifica dello stato a livello di classe
I metodi di classe possono anche agire come costruttori con nome ed effetti collaterali, oppure manipolare variabili di classe che tracciano uno stato condiviso:
class Registry:
_instances = []
def __init__(self, name):
self.name = name
Registry._instances.append(self)
@classmethod
def all(cls):
return list(cls._instances)
@classmethod
def clear(cls):
cls._instances.clear()
Registry("alice")
Registry("bob")
Registry("carol")
print([r.name for r in Registry.all()]) # ['alice', 'bob', 'carol']
Registry.clear()
print(Registry.all()) # []Chiamata da un'Istanza vs dalla Classe
Sia @staticmethod che @classmethod possono essere chiamati su un'istanza o sulla classe. Python gestisce entrambe le forme:
class Circle:
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
def area(self):
return Circle.PI * self.radius ** 2
@classmethod
def unit_circle(cls):
"""Return a circle with radius 1."""
return cls(1)
@staticmethod
def describe():
return "A circle is a round plane figure."
c = Circle(5)
# staticmethod — callable on instance or class
print(c.describe()) # A circle is a round plane figure.
print(Circle.describe()) # A circle is a round plane figure.
# classmethod — callable on instance or class
unit = c.unit_circle()
print(unit.radius) # 1
print(Circle.unit_circle().radius) # 1Chiamarli sulla classe è di solito più chiaro — segnala al lettore che non sono coinvolti dati dell'istanza.
Combinare @classmethod e @staticmethod
Un metodo di classe può delegare il lavoro di validazione a un metodo statico, poiché il metodo di classe ha accesso a cls per chiamarlo:
class PositiveNumber:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"PositiveNumber({self.value})"
@staticmethod
def _validate(value):
if value <= 0:
raise ValueError(f"Expected a positive number, got {value!r}")
@classmethod
def create(cls, value):
cls._validate(value)
return cls(value)
n = PositiveNumber.create(42)
print(n) # PositiveNumber(42)
try:
PositiveNumber.create(-5)
except ValueError as e:
print(e) # Expected a positive number, got -5Riferimento Rapido: Quale Decoratore Usare?
| Situazione | Raccomandazione |
|---|---|
Il metodo legge o scrive self | Metodo di istanza normale |
| Il metodo crea una nuova istanza | @classmethod (factory / costruttore alternativo) |
| Il metodo legge o scrive un attributo di classe | @classmethod |
| Il metodo è un helper puro che non necessita di dati di classe o istanza | @staticmethod (o funzione a livello di modulo) |
| Il metodo valida l'input prima della costruzione | @staticmethod |
| Il metodo deve funzionare correttamente nelle sottoclassi | @classmethod (usa cls, non il nome della classe codificato a parte) |
Errori Comuni
Dimenticare cls dentro @classmethod
Se si codifica a parte il nome della classe invece di usare cls, l'ereditarietà si rompe silenziosamente:
class Animal:
@classmethod
def create(cls):
return cls() # correct — returns an instance of the actual class
class Dog(Animal):
pass
print(type(Dog.create())) # <class '__main__.Dog'> — correctUsa sempre cls(...), mai Animal(...), all'interno di un metodo di classe.
Accedere a self o cls in un @staticmethod
Un @staticmethod non riceve alcun primo argomento implicito. Tentare di fare riferimento a self o cls al suo interno è un errore:
class Bad:
label = "bad"
@staticmethod
def show():
# print(cls.label) # NameError: name 'cls' is not defined
print("use @classmethod if you need cls")
Bad.show() # use @classmethod if you need clsSe ti accorgi di aver bisogno di cls in quello che pensavi fosse un metodo statico, convertilo in un @classmethod.
Confondere i decoratori
I metodi @classmethod devono avere cls come primo parametro esplicito, e i metodi @staticmethod non devono averne nessuno. Scambiarli causa un TypeError al momento della chiamata, non alla definizione — il che può sorprendere:
class Broken:
@staticmethod
def forgot_cls(cls): # cls is just a regular positional argument here
return cls
# Broken.forgot_cls() # TypeError: forgot_cls() missing 1 required positional argument: 'cls'Override nelle sottoclassi
Entrambi i decoratori funzionano con super() e possono essere sovrascritti:
class Base:
@classmethod
def who(cls):
return f"Base.who called with cls={cls.__name__}"
class Child(Base):
@classmethod
def who(cls):
parent = super().who()
return f"Child.who — parent said: {parent}"
print(Child.who())
# Child.who — parent said: Base.who called with cls=ChildNota che cls in Base.who è ancora Child — perché il metodo è stato chiamato da Child.
Esempio Pratico: Una Classe User
Ecco un esempio completo che unisce metodi di istanza, un factory class method e un validatore metodo statico:
import re
class User:
_all_users = []
def __init__(self, name, email):
User._validate_email(email)
self.name = name
self.email = email
User._all_users.append(self)
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r})"
# --- instance method ---
def greet(self):
return f"Hello, my name is {self.name}."
# --- factory / alternative constructor ---
@classmethod
def from_dict(cls, data):
"""Create a User from a dict like {'name': 'Alice', 'email': '[email protected]'}."""
return cls(data["name"], data["email"])
# --- class-level query ---
@classmethod
def count(cls):
return len(cls._all_users)
# --- pure helper, no instance or class data needed ---
@staticmethod
def _validate_email(email):
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, email):
raise ValueError(f"Invalid email address: {email!r}")
# Create via normal constructor
u1 = User("Alice", "[email protected]")
# Create via factory
u2 = User.from_dict({"name": "Bob", "email": "[email protected]"})
print(u1.greet()) # Hello, my name is Alice.
print(u2.greet()) # Hello, my name is Bob.
print(User.count()) # 2
try:
User("Carol", "not-an-email")
except ValueError as e:
print(e) # Invalid email address: 'not-an-email'Questo pattern — __init__ per la costruzione normale, @classmethod per costruttori alternativi, @staticmethod per gli helper — appare in tutta la libreria standard Python (vedi datetime.date.today(), datetime.date.fromisoformat(), int.from_bytes()).
Riepilogo
- Un metodo di istanza riceve
selfe ha pieno accesso allo stato dell'oggetto. - Un
@classmethodricevecls— la classe stessa — invece di un'istanza. Usalo per i factory method e per tutto ciò che opera sullo stato a livello di classe. Usa semprecls(...)al suo interno in modo che le sottoclassi funzionino correttamente. - Un
@staticmethodnon riceve néselfnécls. Usalo per logica di utilità pura che appartiene al namespace della classe ma non necessita di dati dell'oggetto o della classe.
Per gli attributi calcolati che sembrano un normale accesso agli attributi, vedi @property. Per il meccanismo completo dei decoratori che fa funzionare tutti e tre, vedi Decoratori Python.