W3docs

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 PythonDunder chiamato
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__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.

MetodoChiamato daScopo
__repr__repr(), shell interattivaRappresentazione non ambigua, rivolta agli sviluppatori
__str__str(), print(), f-stringVisualizzazione 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.

OperatoreMetodoMetodo 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)  # True

Scorciatoia: 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.celsius

Operatori Aritmetici

I dunder aritmetici permettono ai tuoi oggetti di funzionare con +, -, *, /, //, % e **.

EspressioneMetodoNote
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.

MetodoChiamato daCosa abilita
__len__len(obj)Lunghezza del container
__getitem__obj[index]Accesso per indice e slice
__setitem__obj[index] = valAssegnazione per indice
__delitem__del obj[index]Cancellazione per indice
__contains__item in objTest 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
# cherry

Protocollo 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 da iter(obj) e all'inizio di un ciclo for; deve restituire l'oggetto iteratore (di solito self).
  • __next__ — chiamato ripetutamente per produrre il valore successivo; deve sollevare StopIteration quando 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
# 0

Per 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 items

Questo 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))  # True

double 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 blocco with; il suo valore di ritorno viene assegnato alla variabile as.
  • __exit__(self, exc_type, exc_val, exc_tb) — viene eseguito quando il blocco termina, sia normalmente che tramite un'eccezione. Restituisce True per sopprimere l'eccezione; restituisce False (o None) 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)  # True

Se 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

CategoriaMetodoAttivato 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 classe User sarebbe fonte di confusione.

Per pattern OOP più avanzati, vedi Python Abstract Classes, Python Encapsulation e Python Polymorphism.

Pratica

Pratica
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Was this page helpful?