Fare l'overloading degli operatori significa definire (o ridefinire) il comportamento di un operatore durante l'interazione con un'istanza di una classe che abbiamo creato in precedenza. Questo ci permette di definire cosa succede quando, ad esempio, utilizziamo una sintassi del tipo istanza1 + istanza2
.
Nelle lezioni precedenti, abbiamo visto diversi tipi di operatori: aritmetici (+
, -
, *
, /
, //
, %
), di confronto (==
, !=
, <
, <=
, >
, >=
), binari (<<
, >>
, &
, |
, ^
, ~
), di contenimento (in
e not in
), di indexing (oggetto[indice]
), di accesso a attributi (oggetto.attributo
).
Per ognuno di questi operatori esiste un corrispondente metodo speciale, che può essere definito per specificare il risultato dell'operazione. Per diversi operatori esistono anche due tipi aggiuntivi di metodi speciali, la versione speculare e quella in place. Ad esempio, l'operatore +
ha tre metodi speciali:
__add__
: quando eseguiamoistanza + valore
, viene in realtà eseguito il metodoistanza.__add__(valore)
;__radd__
: quando eseguiamovalore + istanza
, e ilvalore
non definisce un metodo__add__
compatibile con la nostra istanza, viene eseguito il metodoistanza.__radd__(valore)
;__iadd__
: quando eseguiamoistanza += valore
, viene eseguitoistanza.__iadd__(valore)
, permettendoci di modificare l'istanza in place.
Vediamo alcuni esempi di overloading degli operatori:
>>> # definiamo una classe Team
>>> class Team:
... # definiamo un __init__ che assegna i membri all'istanza
... def __init__(self, members):
... self.members = members
... # definiamo un __repr__ che restituisce il tipo dell'oggetto
... # e i nomi dei membri del team
... def __repr__(self):
... names = ', '.join([p.name for p in self.members])
... return '<Team object [{}]>'.format(names)
... # definiamo un __contains__ che restituisce True se un membro
... # fa parte del team, altrimenti False
... def __contains__(self, other):
... return other in self.members
... # definiamo un __add__ che restituisce un nuovo team creato
... # dall'aggiunta di una nuova persona o dall'unione di 2 team
... def __add__(self, other):
... if isinstance(other, Person):
... return Team(self.members + [other])
... elif isinstance(other, Team):
... return Team(self.members + other.members)
... else:
... raise TypeError("Can't add Team with {!r}.".format(other))
... # definiamo un __radd__ che è uguale ad __add__, visto che
... # l'addizione è un'operazione commutativa
... __radd__ = __add__
... # definiamo un __iadd__ che modifica il team aggiungendo una
... # nuova persona o i membri di un altro team al team corrente
... def __iadd__(self, other):
... if isinstance(other, Person):
... self.members.append(other)
... return self
... elif isinstance(other, Team):
... self.members.extend(other.members)
... return self
... else:
... raise TypeError("Can't add {!r} to the team.".format(other))
...
>>>
>>> # creiamo 4 istanze di Person
>>> guido = Person('Guido', 'van Rossum')
>>> tim = Person('Tim', 'Peters')
>>> alex = Person('Alex', 'Martelli')
>>> ezio = Person('Ezio', 'Melotti')
>>>
>>> # creiamo 2 team da 2 persone per team
>>> t1 = Team([guido, tim])
>>> t2 = Team([alex, ezio])
>>>
>>> # verifichiamo i membri dei 2 team
>>> t1
<Team object [Guido, Tim]>
>>> t2
<Team object [Alex, Ezio]>
>>>
>>> # verifichiamo l'overloading dell'operatore in
>>> guido in t1
True
>>> ezio in t1
False
>>> ezio not in t1
True
>>>
>>> # verifichiamo l'overloading dell'operatore + (__add__)
>>> # sommando un'istanza di Team con una di Person
>>> t1 + ezio
<Team object [Guido, Tim, Ezio]>
>>> # verifichiamo che l'operazione ha restituito
>>> # un nuovo team, e che t1 non è cambiato
>>> t1
<Team object [Guido, Tim]>
>>>
>>> # verifichiamo l'overloading dell'operatore + (__radd__)
>>> # sommando un'istanza di Person con una di Team
>>> ezio + t1
<Team object [Guido, Tim, Ezio]>
>>>
>>> # verifichiamo l'overloading dell'operatore + (__add__)
>>> # sommando due istanze di Team
>>> t1 + t2
<Team object [Guido, Tim, Alex, Ezio]>
>>> t2 + t1
<Team object [Alex, Ezio, Guido, Tim]>
>>>
>>> # verifichiamo che t1 contiene 2 membri
>>> t1
<Team object [Guido, Tim]>
>>> # verifichiamo l'overloading dell'operatore += (__iadd__)
>>> # aggiungendo un'istanza di Person al Team t1
>>> t1 += ezio
>>> # verifichiamo che t1 è stato modificato
>>> t1
<Team object [Guido, Tim, Ezio]>
>>>
>>> # creiamo altre 2 istanze di Team
>>> t3 = Team([alex, tim])
>>> t4 = Team([guido, ezio])
>>> # verifichiamo che t3 contiene 2 membri
>>> t3
<Team object [Alex, Tim]>
>>> # verifichiamo l'overloading dell'operatore += (__iadd__)
>>> # aggiungendo un'istanza di Team al Team t3
>>> t3 += t4
>>> # verifichiamo che t3 è stato modificato
>>> t3
<Team object [Alex, Tim, Guido, Ezio]>
>>>
>>> # verifichiamo che aggiungere un tipo incompatibile
>>> # ci restituisce un TypeError
>>> t3 + 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 15, in __add__
TypeError: Can't add Team with 5.
In questo esempio abbiamo definito una classe Team
che implementa diversi metodi speciali: __init__
, __repr__
, __contains__
, __add__
, __radd__
, __iadd__
. Questi metodi ci hanno permesso di definire il comportamento degli operatori in
, not in
, +
, +=
e di verificare i diversi risultati che otteniamo combinandoli con istanze della classe Team
e della classe Person
, definita in un esempio precedente.
Riassunto dei metodi speciali
Le tabelle seguenti riassumono gli operatori più comunemente usati e i loro metodi speciali.
Oltre al corrispondente metodo speciale (es. __add__
), gli operatori aritmetici hanno anche la versione speculare (es. __radd__
) e quella in place (es. __iadd__
):
Operatore | Descrizione | Metodi speciali |
---|---|---|
+ |
addizione | __add__ , __radd__ , __iadd__ |
– |
sottrazione | __sub__ , __rsub__ , __isub__ |
* |
moltiplicazione | __mul__ , __rmul__ , __imul__ |
/ |
divisione | __truediv__ , __rtruediv__ , __itruediv__ |
// |
divisione intera | __floordiv__ , __rfloordiv__ , __ifloordiv__ |
% |
modulo (resto della divisione) | __mod__ , __rmod__ , __imod__ |
Ogni operatore di confronto ha solo un corrispondente metodo speciale:
Operatore | Descrizione | Metodo speciali |
---|---|---|
== |
uguale a | __eq__ |
!= |
diverso da | __ne__ |
< |
minore di | __lt__ |
<= |
minore o uguale a | __le__ |
> |
maggiore di | __gt__ |
>= |
maggiore o uguale a | __ge__ |
Così come gli operatori aritmetici, anche gli operatori binari hanno una versione speculare e una versione in place:
Operatore | Descrizione | Metodi speciali |
---|---|---|
x << n |
esegue uno shift a sinistra di n posizioni dei bit di x |
__lshift__ , __rlshift__ , __ilshift__ |
x >> n |
esegue uno shift a destra di n posizioni dei bit di x |
__rshift__ , __rrshift__ , __irshift__ |
x & y |
esegue un and tra i bit di x e di y |
__and__ , __rand__ , __iand__ |
x | y |
esegue un or tra i bit di x e di y |
__or__ , __ror__ , __ior__ |
x ^ y |
esegue un or esclusivo tra i bit di x e di y |
__xor__ , __rxor__ , __ixor__ |
Esistono anche metodi speciali per determinare il comportamento di un'oggetto durante l'accesso, l'assegnamento, e la rimozione di elementi o attributi:
Operatore | Descrizione | Metodo speciali |
---|---|---|
object[item] |
accesso a un elemento | __getitem__ |
object[item] = value |
assegnamento a un elemento | __setitem__ |
del object[item] |
rimozione di un elemento | __delitem__ |
object.attr |
accesso a un attributo | __getattr__ |
object.attr = value |
assegnamento a un attributo | __setattr__ |
del object.attr |
rimozione di un attributo | __delattr__ |
Esistono infine altri metodi speciali meno comuni, che per brevità non sono inclusi in questa guida, ma che si possono trovare nella documentazione ufficiale sui metodi speciali.
È inoltre importante notare che esistono alcuni operatori su cui non è possibile effettuare l'overloading, in particolare l'operatore di assegnamento (=
), e gli operatori booleani (and
, or
, not
).