Metodi Magici (Dunder) di Python
Impara i metodi magici Python (__init__, __str__, __repr__, overloading degli operatori, protocolli container e context manager) con esempi pratici.
I metodi magici — detti anche metodi dunder (abbreviazione di double-underscore, doppio underscore) — sono metodi speciali il cui nome inizia e termina con due underscore, come __init__ o __len__. Sono il sistema di hook di Python: definendoli nelle proprie classi si indica a Python come un oggetto deve comportarsi con gli operatori e le funzioni built-in come +, len(), print(), in e with.
I metodi dunder non si chiamano mai direttamente. Python li chiama automaticamente dietro le quinte:
| Espressione Python | Dunder chiamato |
|---|---|
str(obj) | obj.__str__() |
len(obj) | obj.__len__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
item in obj | obj.__contains__(item) |
with obj as x: | obj.__enter__() / obj.__exit__(...) |
Questo capitolo tratta:
- Rappresentazione come string —
__repr__e__str__ - Operatori di confronto —
__eq__,__lt__e simili - Operatori aritmetici —
__add__,__mul__,__rmul__e altri - Protocollo container —
__len__,__getitem__,__contains__ - Protocollo iteratore —
__iter__e__next__ - Valore di verità —
__bool__ - Oggetti chiamabili —
__call__ - Protocollo context manager —
__enter__e__exit__ __hash__— rendere gli oggetti utilizzabili come chiavi di dizionario
Prima di leggere, assicurati di avere familiarità con le classi e oggetti Python e con l'ereditarietà Python. Per gli attributi calcolati, consulta @property.
Rappresentazione come stringa: __repr__ e __str__
Questi due metodi controllano come un oggetto viene convertito in stringa.
| Metodo | Chiamato da | Scopo |
|---|---|---|
__repr__ | repr(), shell interattiva | Rappresentazione non ambigua, rivolta agli sviluppatori |
__str__ | str(), print(), f-string | Visualizzazione leggibile dall'utente |
Se __str__ non è definito, Python ricade su __repr__. È quindi buona pratica definire sempre __repr__ e definire __str__ solo quando si desidera un formato diverso leggibile dall'utente.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
def __str__(self):
return f'"{self.title}" by {self.author} ({self.pages} pages)'
b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b)) # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b)) # "Clean Code" by Robert C. Martin (431 pages)
print(b) # "Clean Code" by Robert C. Martin (431 pages)Il flag di conversione !r all'interno di una f-string chiama repr() su quel valore, aggiungendo le virgolette intorno alle stringhe. Questo rende l'output di __repr__ codice Python incollabile direttamente.
Suggerimento: un buon __repr__ permette di ricostruire l'oggetto dal suo output. Pensa a eval(repr(obj)) == obj come modello mentale (anche quando non è letteralmente vero).
Operatori di Confronto
Tutti gli operatori di confronto Python corrispondono a metodi dunder. Definiscili quando vuoi che ==, <, >, <= o >= confrontino i tuoi oggetti in modo significativo.
| Operatore | Metodo | Metodo riflesso |
|---|---|---|
== | __eq__ | __eq__ |
!= | __ne__ | __ne__ |
< | __lt__ | __gt__ |
<= | __le__ | __ge__ |
> | __gt__ | __lt__ |
>= | __ge__ | __le__ |
Il metodo riflesso viene chiamato da Python sull'operando destro quando quello sinistro restituisce NotImplemented. Ad esempio, se a < b chiama a.__lt__(b) e questo restituisce NotImplemented, Python prova allora il metodo riflesso b.__gt__(a).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __le__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius <= other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t2 > t1) # True — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3) # TrueScorciatoia: se vuoi solo che gli oggetti siano ordinabili senza preoccuparti dei singoli sei operatori, usa il decoratore @functools.total_ordering. Definisci __eq__ e uno tra __lt__, __le__, __gt__ o __ge__, e total_ordering aggiunge automaticamente gli altri.
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsiusOperatori Aritmetici
I dunder aritmetici permettono ai tuoi oggetti di funzionare con +, -, *, /, //, % e **.
| Espressione | Metodo | Note |
|---|---|---|
a + b | __add__ | |
a - b | __sub__ | |
a * b | __mul__ | |
b * a | __rmul__ | versione a destra; chiamato quando b.__mul__(a) restituisce NotImplemented |
-a | __neg__ | negazione unaria |
abs(a) | __abs__ | |
a += b | __iadd__ | in-place; ricade su __add__ se non definito |
Un caso d'uso classico è una classe vettore 2D:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # supports: 3 * v
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — uses __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0__rmul__ è ciò che permette a 3 * v1 di funzionare. Quando Python valuta 3 * v1, chiama prima int.__mul__(3, v1). La classe intera built-in non sa come moltiplicare un intero per un Vector, quindi restituisce NotImplemented. Python prova allora il metodo riflesso: v1.__rmul__(3), che riesce.
Protocollo Container
Implementa questi metodi per fare in modo che la tua classe si comporti come una sequenza o una collezione.
| Metodo | Chiamato da | Cosa abilita |
|---|---|---|
__len__ | len(obj) | Lunghezza del container |
__getitem__ | obj[index] | Accesso per indice e slice |
__setitem__ | obj[index] = val | Assegnazione per indice |
__delitem__ | del obj[index] | Cancellazione per indice |
__contains__ | item in obj | Test di appartenenza |
Definire __len__ insieme a __getitem__ è sufficiente a rendere la classe automaticamente iterabile — il ciclo for di Python chiamerà __getitem__ con indici successivi a partire da 0 fino a ottenere un IndexError.
class WordBag:
def __init__(self, *words):
self._words = list(words)
def __len__(self):
return len(self._words)
def __contains__(self, item):
return item in self._words
def __getitem__(self, index):
return self._words[index]
def __repr__(self):
return f"WordBag({self._words!r})"
bag = WordBag("apple", "banana", "cherry")
print(len(bag)) # 3
print("banana" in bag) # True
print("grape" in bag) # False
print(bag[0]) # apple
print(bag[-1]) # cherry
# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
print(word)
# apple
# banana
# cherryProtocollo Iteratore
Se vuoi un comportamento completo da iteratore (funzionare con iter() e next() direttamente, o essere utilizzabile in contesti che richiedono un iteratore e non semplicemente un iterabile), definisci sia __iter__ che __next__:
__iter__— chiamato daiter(obj)e all'inizio di un ciclofor; deve restituire l'oggetto iteratore (di solitoself).__next__— chiamato ripetutamente per produrre il valore successivo; deve sollevareStopIterationquando esaurito.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
# 3
# 2
# 1
# 0Per pattern di iterazione più potenti — in particolare sequenze lazy che producono valori su richiesta — vedi Python Generators e Python Iterators.
Valore di Verità: __bool__
Python chiama __bool__ quando un oggetto viene usato in un contesto boolean (un'istruzione if, un ciclo while, not, and, or). Se __bool__ non è definito ma lo è __len__, Python usa len(obj) != 0 come valore di verità. Se nessuno dei due è definito, l'oggetto è sempre truthy.
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
return self._data.pop()
def __len__(self):
return len(self._data)
def __bool__(self):
return len(self._data) > 0
def __repr__(self):
return f"Stack({self._data!r})"
s = Stack()
print(bool(s)) # False — empty stack is falsy
s.push(1)
print(bool(s)) # True
print(len(s)) # 1
if s:
print("stack has items") # stack has itemsQuesto rispecchia il comportamento delle collezioni built-in: una lista, un dizionario o un set vuoti sono falsy; uno non vuoto è truthy.
Oggetti Chiamabili: __call__
Definire __call__ permette di usare un'istanza come se fosse una funzione. Questo è utile per oggetti che mantengono uno stato tra le chiamate — qualcosa che una semplice funzione non può fare senza una closure o una variabile globale.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # Truedouble e triple sono oggetti ordinari, ma vengono chiamati con () proprio come le funzioni. La funzione built-in callable() restituisce True per qualsiasi oggetto che abbia __call__.
Questo pattern è comune nei framework di machine learning (layer, funzioni di perdita) e nelle factory di decoratori. Vedi Python Decorators per un caso d'uso strettamente correlato.
Protocollo Context Manager: __enter__ e __exit__
L'istruzione with è il modo di Python per configurare e rilasciare una risorsa in modo affidabile — anche in caso di eccezione. Qualsiasi oggetto che definisca __enter__ e __exit__ può essere usato come context manager.
__enter__(self)— viene eseguito quando inizia il bloccowith; il suo valore di ritorno viene assegnato alla variabileas.__exit__(self, exc_type, exc_val, exc_tb)— viene eseguito quando il blocco termina, sia normalmente che tramite un'eccezione. RestituisceTrueper sopprimere l'eccezione; restituisceFalse(oNone) per lasciarla propagare.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file # the value bound to the "as" variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False # do not suppress exceptions
with ManagedFile("/etc/hostname") as f:
content = f.read()
# The file is guaranteed to be closed here, even if an exception occurred inside the block.Il decoratore contextlib.contextmanager della libreria standard permette di scrivere la stessa logica come funzione generatore — un'alternativa più leggera per i casi semplici. Vedi Python with Statement per una trattazione completa.
Hashing: __hash__
Python usa __hash__ per inserire gli oggetti in set e dizionari. Il __hash__ predefinito si basa sull'indirizzo in memoria dell'oggetto (identità). Quando si sovrascrive __eq__, Python imposta automaticamente __hash__ a None, rendendo gli oggetti non hashable — è necessario definire __hash__ esplicitamente se si vuole che funzionino in set o come chiavi di dizionario.
La regola è: oggetti che risultano uguali devono avere lo stesso hash.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # hash of an immutable tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 is p2) # False — different objects in memory
print(hash(p1) == hash(p2)) # True
seen = {p1, p2, p3}
print(len(seen)) # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen) # TrueSe la tua classe è mutabile (i suoi campi possono cambiare dopo la creazione), non definire __hash__. Gli oggetti mutabili non dovrebbero essere hashable perché modificarne i campi cambierebbe il loro hash, rompendo qualsiasi set o dizionario che li contenga già.
Tabella Riepilogativa
| Categoria | Metodo | Attivato da |
|---|---|---|
| Rappresentazione | __repr__ | repr(obj), shell interattiva |
| Rappresentazione | __str__ | str(obj), print(obj), f-string |
| Confronto | __eq__, __ne__ | ==, != |
| Confronto | __lt__, __le__, __gt__, __ge__ | <, <=, >, >= |
| Aritmetica | __add__, __sub__, __mul__ | +, -, * |
| Aritmetica | __rmul__, __radd__, … | forme riflesse a destra |
| Aritmetica | __neg__, __abs__ | - unario, abs() |
| Container | __len__ | len(obj) |
| Container | __getitem__, __setitem__, __delitem__ | obj[i], obj[i] = v, del obj[i] |
| Container | __contains__ | item in obj |
| Iteratore | __iter__ | iter(obj), ciclo for |
| Iteratore | __next__ | next(obj) |
| Valore di verità | __bool__ | bool(obj), if obj: |
| Chiamabile | __call__ | obj(args) |
| Context manager | __enter__, __exit__ | with obj as x: |
| Hashing | __hash__ | hash(obj), chiavi dict, set |
Quando Usare i Metodi Magici
- Usali quando la tua classe rappresenta un tipo valore (un punto, un vettore, un importo monetario, un intervallo di date) — l'overloading degli operatori e dei confronti rende la classe naturale da usare.
- Usali quando la tua classe racchiude una risorsa (un file, una connessione al database, un socket di rete) —
__enter__/__exit__garantisce che la risorsa venga sempre rilasciata. - Usali quando la tua classe è una collezione personalizzata — i protocolli container e iteratore la rendono compatibile con
for,in,len()e le list comprehension. - Evitali per le classi applicative ordinarie che non sono tipi valore né container. Fare l'overloading di
+su una classeUsersarebbe fonte di confusione.
Per pattern OOP più avanzati, vedi Python Abstract Classes, Python Encapsulation e Python Polymorphism.