
Overloading in Python
In questa guida parliamo di overloading in Python di metodi nelle classi o di funzioni.
Cos'è l'overloading? L'overloading, o sovraccarico, è un concetto che ci permette di definire nella stessa classe più metodi con lo stesso nome ma con parametri diversi. Questo ci dà la flessibilità di chiamare lo stesso metodo in modi differenti, a seconda degli argomenti che passiamo. L'overloading può essere applicato anche sulle funzioni.
Il linguaggio Python non supporta l'overloading dei metodi nel modo tradizionale come fanno altri linguaggi come Java o C++, ma possiamo ottenere risultati simili in modi creativi.
In particolar modo, alcune tecniche ti permettono di ottenere un risultato simile. Esploriamo due approcci alternativi: l'uso di argomenti predefiniti e il pacchetto.
Uso di argomenti predefiniti e argomenti variabili
Un modo per simulare l'overloading in Python è utilizzare argomenti predefiniti e argomenti variabili.
Questo permette di chiamare un metodo con un numero variabile di argomenti e gestire diverse situazioni con un unico metodo o con un'unica funzione.
Esempio di una funzione
Ad esempio, supponi di voler scrivere una funzione che può sommare due o tre numeri. Potresti definirla così:
def somma(a, b, c=None):
if c is None:
return a + b
else:
return a + b + c
A seconda se passi due o tre argomenti, la funzione esegue un calcolo oppure un altro.
print(somma(2, 3))
5
Se invece passi tre argomenti:
print(somma(2, 3, 4))
9
In questo caso, hai definito un parametro opzionale `c`, che se non viene specificato, viene considerato come `None`, e la funzione si comporta come una semplice somma di due numeri.
Esempio di metodo in una classe
Lo stesso approccio puoi applicarlo a un metodo di una classe. Del resto un metodo è una funzione locale all'interno della classe.
Ad esempio, crea una classe `Calcolatrice` che ha un metodo `somma` capace di sommare due o tre numeri a seconda degli argomenti passati.
class Calcolatrice:
def somma(a, b, c=None):
if c is None:
return a + b
else:
return a + b + c
Poi crea di un'istanza della classe Calcolatrice
calc = Calcolatrice()
Ora chiama il metodo somma() con due argomenti
print(calc.somma(2, 3))
5
Infine, chiama lo stesso metodo ma con tre argomenti
print(calc.somma(2, 3, 4))
9
Come vedi, il metodo somma() della classe `Calcolatrice` puoi chiamarlo sia con due che con tre argomenti numerici.
Se gli argomenti sono due, il metodo somma quei due numeri. Se gli fornisci anche il terzo argomento, il metodo somma tre argomenti.
Uso di argomenti variabili
Un altro approccio, un po' più flessibile, è usare gli argomenti variabili, che ti permettono di passare un numero arbitrario di argomenti al metodo.
Ad esempio, crea una classe 'Calcolatrice'
class Calcolatrice:
def somma(self, *numeri):
"""Somma un numero arbitrario di argomenti."""
return sum(numeri)
In questo caso il metodo somma() nella classe `Calcolatrice` può accettare un numero variabile di argomenti grazie all'uso del parametro `*numeri`, che raccoglie tutti gli argomenti passati in una tupla.
Il metodo poi usa la funzione sum() per sommare tutti gli elementi della tupla.
Facciamo una prova, crea un'istanza della classe Calcolatrice
calc = Calcolatrice()
Chiama il metodo somma() passandogli due argomenti
print(calc.somma(2, 3))
5
Poi con tre argomenti
print(calc.somma(2, 3, 4))
9
Infine, con quattro argomenti
print(calc.somma(1, 2, 3, 4, 5))
15
In questo modo puoi usare lo stesso metodo per sommare un numero indefinito di argomenti.
Uso del pacchetto `functools`
Un altro approccio per ottenere gli effetti dell'overloading è usare i decoratori del modulo `functools`.
Ti permette di creare funzioni o metodi sovraccaricabili basate sul tipo di uno o più argomenti.
Esempio di una funzione
Supponi di voler scrivere una funzione che gestisce diversamente l'input basandosi sul tipo di dato.
In questo caso puoi usare il decoratore @singledispatch per definire la funzione base e poi aggiungere comportamenti specifici per diversi tipi di dati.
from functools import singledispatch
@singledispatch
def descrivi(tipo):
print(f"Il tipo generico è {type(tipo)}")
@descrivi.register(int)
def _(tipo):
print(f"Il numero intero è {tipo}")
@descrivi.register(list)
def _(tipo):
print(f"La lista contiene {len(tipo)} elementi")
Quando chiami la funzione descrivi(), questa ha un comportamento diverso a seconda del tipo di dato che inserisci come argomento.
Se passi un numero intero restituisce un output
descrivi(42)
Il numero intero è 42
Se invece passi una lista restituisce un altro output
descrivi([1, 2, 3, 4])
La lista contiene 4 elementi
In pratica, la funzione descrivi() si comporta in modo diverso a seconda che riceva un intero o una lista.
Puoi aggiungere altri comportamenti specifici a seconda delle tue esigenze.
Come associare più tipi all'argomento della funzione?
Per associare più tipi a un argomento, devi decorare più volte la funzione in cascata.
Ad esempio, per intercettare tipo 'float' e 'int' basta scrivere:
@descrivi.register(float)
@descrivi.register(int)
def _(tipo):
print(f"Il numero intero è {tipo}")
Ricorda che il decoratore @singledispatch lavora solo sul primo argomento della funzione.
Come richiamare una funzione esistente in base all'argomento?
In Python è anche possibile associare un tipo di dato di un argomento a una funzione già esistente.
Ad esempio, se l'argomento della funzione descrivi() è una stringa, viene richiamata la funzione foo()
from functools import singledispatch
@singledispatch
def descrivi(tipo):
print(f"Il tipo generico è {type(tipo)}")
@descrivi.register(int)
def _(arg1):
print(f"Il numero intero è {arg1}")
def foo(arg1):
print('La stringa è', arg1)
descrivi.register(str, foo)
Pertanto, se chiami la funzione descrivi() passandogli un numero intero, questa risponde in questo modo:
descrivi(1)
Il numero intero è 1
Se invece chiami la funzione descrivi() con una stringa, viene richiamata la funzione foo() e il risultato è il seguente:
descrivi('ciao')
La stringa è ciao
Esempio di un metodo di una classe
Il decoratore `singledispatch` è progettato per funzionare con funzioni al livello del modulo e non puoi usarlo direttamente con i metodi di classe.
Tuttavia, puoi aggirare questa limitazione usando il decoratore @singledispatchmethod.
Il decoratore @singledispatchmethod è una versione di @singledispatch progettata per essere utilizzata con metodi di classe. Ti permette di definire un metodo di base che viene poi specializzato per diversi tipi di dati attraverso metodi aggiuntivi registrati.
Ad esempio, supponi di avere una classe che gestisce diversi tipi di input per eseguire una certa operazione.
In questo caso, crea una classe `Calcolatrice` che ha un metodo `descrivi` che si comporta diversamente a seconda del tipo di dato fornito.
from functools import singledispatchmethod
class Calcolatrice:
@singledispatchmethod
def descrivi(self, dato):
raise NotImplementedError("Non supportato")
@descrivi.register(int)
def _descrivi_int(self, dato):
return f"Numero intero processato: {dato}"
@descrivi.register(str)
def _descrivi_str(self, dato):
return f"Stringa processata: {dato.upper()}"
@descrivi.register(list)
def _descrivi_list(self, dato):
return f"Lista processata con {len(dato)} elementi"
Adesso crea un'istanza della classe Calcolatrice.
proc = Calcolatrice()
Chiama il metodo descrivi() con diversi tipi di input, ad esempio con un numero intero.
print(proc.descrivi(123))
Numero intero processato: 123
Ora chiama il metodo passandogli una stringa come argomento.
print(proc.descrivi("ciao"))
Stringa processata: CIAO
Infine, chiama il metodo passandogli una lista
print(proc.descrivi([1, 2, 3, 4]))
Lista processata con 4 elementi
L'uso di singledispatchmethod ti aiuta a gestire molteplici tipi di dati all'interno di una stessa classe.
In questo modo ciascun tipo ha una sua specifica implementazione.
Overloading delle operazioni aritmetiche
Il linguaggio Python ti permette anche di definire un comportamento personalizzato degli operatori aritmetici all'interno degli oggetti di una classe.
In altre parole, ti consente di specificare come devono comportarsi gli operatori aritmetici (come +, -, *, ecc.) quando vengono utilizzati con istanze di una classe personalizzata.
Ad esempio, supponi di avere una classe `Vector2D` che rappresenta un vettore bidimensionale.
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
Ora crea due oggetti della classe.
v1 = Vector2D(2, 3)
v2 = Vector2D(4, 5)
Questi oggetti non puoi sommarli usando l'operatore dell'addizione '+'
v3=v1+v2
TypeError: unsupported operand type(s) for +: 'Vector2D' and 'Vector2D'
Per superare questo problema, puoi sovrascrivere il comportamento dell'operatore aritmetico '+' all'interno della classe usando il metodo '__add__'.
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
Ora crea le nuove istanze.
v1 = Vector2D(2, 3)
v2 = Vector2D(4, 5)
Se ora provi a sommarle, Python esegue il metodo __add__ quando incontra l'operatore '+' e ti restituisce la somma vettoriale.
v3=v1+v2
Il risultato finale è un altro oggetto del tipo Vector2D con i valori (6, 8).
Ti spiego cosa è accaduto. La somma di due vettori si ottiene sommando le rispettive componenti x e y.
Puoi accedere alle singole componenti accedendo agli attributi 'x' e 'y'
print(v3.x, v3.y)
6 8
In questo modo hai sovrapposto il comportamento dell'operatore '+' con il metodo '__add__' ma solo all'interno degli oggetti della classe Vector2D.
L'overloading degli operatori aritmetici ti consente di utilizzare una sintassi intuitiva e naturale per operazioni complesse.
In generale, puoi usare lo stesso approccio per sommare, sottrarre e calcolare il prodotto scalare di due vettori utilizzando gli operatori `+`, `-` e `*`.
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector2D(self.x - other.x, self.y - other.y)
def __mul__(self, other):
return self.x * other.x + self.y * other.y # Prodotto scalare
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
In questo modo quando scrivi `v1 - v2`, Python chiama `v1.__sub__(v2)`, creando un nuovo vettore con componenti pari alla differenza delle componenti dei vettori originali.
Scrivendo `v1 * v2`, invece, Python chiama `v1.__mul__(v2)`, calcolando il prodotto scalare dei due vettori.
Infine, il metodo __repr__ indica a Python come rappresentare il contenuto dell'oggetto quando viene visualizzato. Ad esempio, quando esegui l'istruzione print(v3) viene restituita la stringa "Vector2D(1, 2)" anziché il riferimento di memoria dell'oggetto.
Le operazioni commutative
Quando fai l'overloading delle operazioni aritmetiche devi ricordare che i metodi non sono commutativi.
Ad esempio, prendiamo il caso del prodotto di un vettore per uno scalare.
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})"
L'overloading dle metodo "__mul__()" ti permette di usare l'operatore * per moltiplicare un vettore per un numero.
v = Vector(1, 2)
print(v * 3)
Python riconosce che il primo operando è un vettore e il secondo è uno scalare, quindi richiama l'overloading del metodo __mul__() e restituisce il risultato.
Quindi, il risultato dell'operazione v*3 è il vettore Vector(3,6) ossia il vettore Vector(1,2) con le componenti x e y moltiplicate per tre.
Vector(3, 6)
Tuttavia, se provi a moltiplicare un numero scalare per un vettore, Python non chiama l'overloading di __mul__() perché il primo operando è uno scalare, non è un vettore.
In questo caso l'operazione 3*v restituisce 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__()" che 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})"
Ora quando esegui il prodotto di un vettore per uno scalare funziona sia se il vettore è a sinistra del numero scalare, sia se è a destra.
v = Vector(1, 2)
print(v * 3)
print(3 * v)
Vector(3, 6)
Vector(3, 6)
Allo stesso modo puoi rendere commutativa l'operazione di overloading dell'addizione usando il metodo "__radd__()".
L'attributo dispatch
L'attributo dispatch ti permette di chiamare la funzione associata a un particolare tipo.
Ad esempio, digita questa funzione
from functools import singledispatch
@singledispatch
def descrivi(tipo):
print(f"Il tipo dell'argomento è {type(tipo)}")
@descrivi.register(int)
def _(arg1):
print(f"Il numero intero è {arg1}")
@descrivi.register(str)
def _(arg1):
print(f"La stringa è {arg1}")
La funzione descrivi() si comporta in modo diverso a seconda se l'argomento è un intero (int) o una stringa (str).
Se vuoi chiamare la funzione associata agli interi (int), indipendentemente dal tipo dell'argomento, puoi usare l'attributo dispatch.
fun=descrivi.dispatch(int)
In alternativa, puoi usare l'attributo "registry" dove sono memorizzate tutte le associazioni dei tipi alla funzione. Il risultato è lo stesso.
fun=descrivi.registry[int]
Ora richiama la funzione fun() passandogli come argomento una stringa
fun("uno")
Anche se l'argomento è una stringa ("uno") l'attributo dispatch chiama la funzione associata agli interi
Il numero intero è uno.
Nota che se l'attributo dispatch() non trova il tipo associato, allora chiama l'implementazione principale associata all'oggetto.
fun=descrivi.dispatch(float)
fun("uno")
Il tipo dell'argomento è <class 'str'>
E' un ulteriore possibilità offerta dal linguaggio Python che è utile conoscere.