Overloading delle operazioni matematiche in Python

Python ti permette di implementare e personalizzare le operazioni matematiche all'interno di una classe tramite l'overloading.

Cos'è l'overloading? L'overloading (o sovraccarico) è la capacità di definire più versioni di un metodo o operatore con lo stesso nome ma con differenti tipi o numeri di parametri. In Python, l'overloading è spesso implementato utilizzando i metodi magici per personalizzare il comportamento degli operatori standard quando applicati agli oggetti delle classi definite dall'utente.

In altre parole, ti consente di usare gli operatori matematici come +, -, *, ecc. e indicare come devono comportarsi quando sono usati sugli oggetti di una classe.

Ti faccio un esempio pratico.

L'operatore di assegnazione e incremento += è usato per aggiungere un valore a quello già presente in una variabile numerica.

Ad esempio, assegna il numero 1 alla variabile num.

num=1

Poi usa l'operatore += per aggiungere 2 al valore già esistente.

num+=2

Il risultato finale è 1+2=3

print(num)

3

L'operatore di assegnazione e incremento += funziona con i valori numerici ma non funziona se vuoi usarlo per sommare i vettori.

Puoi però implementare l'operatore += all'interno di una classe definendo il metodo speciale __iadd__() che viene invocato quando si utilizza l'operatore += su un'istanza della classe.

L'uso di __iadd__ ti permette di definire anche un comportamento personalizzato per l'operatore += in una classe.

Ad esempio, crea una classe Vector che rappresenta un vettore nello spazio bidimensionale (2D).

Poi implementa il metodo __iadd__() per sommare un altro vettore o una coppia di coordinate (x, y) al vettore corrente.

class Vector:
    def __init__(self, x: float = 0.0, y: float = 0.0):
        self.x = x
        self.y = y

    def __iadd__(self, other):
        if isinstance(other, Vector):
            # Se other è un oggetto di tipo Vector, somma le coordinate corrispondenti
            self.x += other.x
            self.y += other.y
        elif isinstance(other, (tuple, list)) and len(other) == 2:
            # Se other è una tupla o una lista con due elementi, somma i valori di x e y
            self.x += other[0]
            self.y += other[1]
        else:
            raise TypeError("Il tipo di other deve essere Vector o una tupla/lista di due numeri")
        
        # Restituisce l'oggetto corrente per supportare il chaining
        return self

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

È importante restituire self alla fine del metodo per garantire che l'operazione avvenga in-place, modificando l'istanza corrente. Questa tecnica è utile soprattutto quando si lavora con oggetti mutabili.

Definisci due oggetti v1 e v2 di tipo Vector

v1 = Vector(1, 2)
v2 = Vector(3, 4)

Poi usa l'operatore di assegnazione e incremento += per sommare il secondo vettore al primo vettore

v1 += v2

Ora stampa il risultato

print(v1)

Vector(4, 6)

Al primo vettore è stato aggiunto il secondo.

$$ \vec{v}_1 + \vec{v}_2 = \begin{pmatrix}  1 \\ 2 \end{pmatrix} + \begin{pmatrix}  3 \\ 4 \end{pmatrix} = \begin{pmatrix}  1+3 \\ 2+4 \end{pmatrix} = \begin{pmatrix}  4 \\ 6 \end{pmatrix} $$

Puoi anche sommare un vettore e una tupla (x, y)

v1 += (1, 1)
print(v1)

Vector(5, 7)

Infine, puoi sommare un vettore e una lista (x, y)

v1 += [2, 3]
print(v1)

Vector(7, 10)

Ricorda però che l'operazione non è commutativa.

Il metodo __iadd__() viene invocato solo se il primo operando è un oggetto della classe Vector dove hai implementato il metodo speciale.

Ad esempio, se il primo operando è una tupla, l'operazione solleva un'eccezione.

v3=(1,1)
v3 += v1

Traceback (most recent call last):
TypeError: can only concatenate tuple (not "Vector") to tuple

Usando questa tecnica puoi implementare e personalizzare tutti gli operatori matematici.

Ecco una lista completa dei metodi speciali che puoi utilizzare nel linguaggio Python

Metodo magico Significato
__add__ +
__sub__ -
__mul__ *
__floordiv__ //
__div__ /
__truediv__ /
__mod__ %
__divmod__ divmod(a, b)
__pow__ ** pow(a, power, modulo=None)
__lshift__ <<
__rshift__ >>
__and__ &
__xor__ ^
__or__ |
__neg__ -
__pos__ +
__abs__ abs(a)
__invert__ ~

Le operazioni commutative

Un aspetto da ricordare è che l'overloading delle operazioni aritmetiche non è commutativo.

Ad esempio, implementa il prodotto di un vettore per uno scalare nella class Vector tramite il metodo __mul__()

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

Il sovraccarico del metodo __mul__() ti consente di utilizzare l'operatore * per moltiplicare un vettore per un numero scalare.

v = Vector(1, 2)
print(v * 3)

Quando invochi il metodo nell'operazione v*3, Python riconosce che il primo operando (v) è un vettore mentre il secondo operando (3) è un numero scalare.

Quindi richiama l'overloading del metodo __mul__() nella classe Vector e restituisce il risultato corretto.

Vector(3, 6)

Se però digiti 3*v Python non trova un vettore al primo operando dell'operazione di moltiplicazione.

In questo caso Python non chiama l'overloading di __mul__() restituendo un errore.

v = Vector(1, 2)
print(3 * v)

Traceback (most recent call last):
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

Per rendere la moltiplicazione commutativa devi implementare anche il metodo "__rmul__()" nella classe Vector.

Questo metodo inverte l'ordine degli operandi da destra verso sinistra.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return

    def __rmul__(self, scalar):
        # Si utilizza lo stesso metodo per rendere il prodotto commutativo
        return self.__mul__(scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

Adesso il prodotto di un vettore per uno scalare funziona sia se il vettore è il primo operando, sia se è il secondo operando.

v = Vector(1, 2)
print(v * 3)
print(3 * v)

Vector(3, 6)
Vector(3, 6)

Utilizzando questa tecnica puoi rendere commutativa l'operazione di overloading dell'addizione usando il metodo "__radd__()".

Nota. Ricorda però che i metodi inversi (es. __radd__(), __rmul__(), ecc. ) esistono solo per alcune operazioni matematiche e non per tutte.  Ad esempio, il metodo __iadd__() non ha un metodo inverso di default ma va costruito.

 




Se qualcosa non ti è chiaro, scrivi la tua domanda nei commenti.




FacebookTwitterLinkedinLinkedin