Vai al contenuto principale

Introduzione ai magic method di Python

Scopri cosa sono i magic method, dando un'occhiata alla differenza tra metodi e funzioni.
· 11 min leggi

Scopri con l'IA

Uno dei fattori che rende Python un linguaggio di programmazione così potente è la presenza dei magic method. In alcuni ambienti Pythonista potresti sentirli chiamare "dunder methods" per via della loro struttura con doppio underscore. 

Il loro ruolo in Python è eseguire una procedura nota come "sovraccarico degli operatori" (operator overloading), che significa semplicemente attribuire un significato esteso a un operatore predefinito. In altre parole, il sovraccarico degli operatori serve ad aggiungere comportamenti personalizzati alle nostre classi per abilitarne l'uso con gli operatori di Python e le sue funzionalità built-in.  

In questo articolo approfondiremo i magic method di Python: 

  • Distinguendo la differenza tra metodi e funzioni 
  • Spiegando cosa sono i magic method
  • Fornendo alcuni esempi in Python. 

Dai un'occhiata a questo DataLab workbook per seguire il codice usato in questo tutorial. 

Metodi Python vs Funzioni

Prima di entrare nel merito dei magic method, è importante capire le differenze tra un metodo e una funzione in Python. 

Python usa i metodi per rappresentare il comportamento di un oggetto. I metodi sono quindi associati a un oggetto ed eseguono alcune operazioni sui dati presenti in un oggetto. Un altro fattore distintivo di un metodo è il parametro self, che deve sempre comparire come primo parametro passato al metodo. 

Nota: non deve per forza chiamarsi self come primo parametro, ma i Pythonisti concordano universalmente che sia una best practice. 

# Example of a method
class Human:
    # This is a method
    def walk(self):
        print("I am walking") 

I metodi dipendono dalla classe con cui sono associati. Pertanto, per chiamare un metodo, è necessario creare un'istanza della classe prima di accedere al metodo con l'operatore punto. 

# This will work
joe = Human()
joe.walk()

"""
I am walking
"""




# This will throw a NameError
walk()

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_153/2327344440.py in <module>
      4
      5 # This will throw a NameError
----> 6 walk()

NameError: name 'walk' is not defined

Una funzione, invece, è diversa: non dobbiamo creare un'istanza di una classe. 

Le funzioni Python sono passi eseguiti come un blocco di codice solo quando vengono chiamate. Sono definite in modo indipendente da qualsiasi oggetto classe, quindi possono essere chiamate direttamente per nome. Non essendo associate ad alcuna classe, non dobbiamo passare self come parametro – i parametri sono opzionali. 

In sostanza, una funzione è semplicemente un insieme di istruzioni ben definite che l'interprete utilizza per svolgere un compito. 

# Example of a function
def run():
  print("I am running.")
​
run()

"""
I am running.
"""

Come approfondito nel nostro articolo How to call and write functions, le funzioni si presentano in due varianti: built-in e definite dall'utente. 

  • Funzioni built-in: funzioni incluse nella libreria standard di Python. 
  • Funzioni definite dall'utente: funzioni definite dall'utente – Python permette di definire e usare funzioni personalizzate accessibili in un programma Python.  

Sotto molti aspetti, metodi e funzioni sono identici, ad esempio sono entrambi definiti con un'istruzione def. La differenza sostanziale è che i metodi sono associati a un oggetto e servono a operare sui dati contenuti all'interno di una classe, mentre le funzioni sono definite in modo indipendente e possono essere chiamate per nome ovunque in un programma. 

Cosa sono i magic method di Python?

All'inizio del tutorial abbiamo menzionato che i magic method sono anche chiamati dunder methods a causa del loro doppio underscore. Con questo intendiamo che tutti i magic method di Python iniziano e finiscono con due underscore. 

Il magic method più noto è __init__, che consente di inizializzare una classe con attributi specifici. 

class Employee:
    def __init__(self, name, age, role):
        self.name = name
        self.age = age
        self.role = role

Per istanziare un oggetto di questa classe, dobbiamo specificare gli attributi name, age e role dell'istanza. Per esempio:

user_1 = Employee(name="John", age=24, role="Data Scientist")

In caso contrario verrebbe sollevata una TypeError. 

Il metodo __init__ rientra nella categoria dei magic method di inizializzazione e costruzione, ma è solo uno dei tanti. La tabella seguente illustra altri magic method di inizializzazione e costruzione. 

__new__(cls, other)

Usato per creare una nuova istanza di una classe

__init__(self, other)

Chiamato dopo la creazione di una nuova istanza; tutti gli attributi passati all'espressione del costruttore della classe verranno usati come attributi. 

__del__(self)

Conosciuto anche come distruttore; chiamato quando un'istanza sta per essere distrutta. 

Il modo più semplice per concettualizzare i magic method è considerarli come un contratto tra la tua implementazione e l'interprete Python. Una delle clausole del contratto richiede a Python di eseguire alcune azioni dietro le quinte in determinate circostanze, come inizializzare un'istanza di classe con oggetti specifici. 

In altre parole, non è tua responsabilità invocare direttamente un magic method chiamandolo: Python lo fa per te dietro le quinte. 

Metodi di rappresentazione testuale

Python è un linguaggio di programmazione orientata agli oggetti (OOP) – in Python tutto è un oggetto. Quando crei un nuovo oggetto, crei implicitamente un oggetto correlato perché tutte le classi ereditano da object.  

Nota: puoi anche rendere l'ereditarietà esplicita passando la classe object alla tua classe. 

# This works
class Car:
  pass
​
# This also works
class Vehicle(object):
  pass
​
car = Car()
vehicle = Vehicle()
​
print(car, vehicle, sep="\n")

"""
<__main__.Car object at 0x7f80449dc0a0>
<__main__.Vehicle object at 0x7f80449dc1f0>
"""

Nel codice sopra abbiamo creato due classi vuote, ma l'interprete Python sapeva comunque cosa stampare quando abbiamo chiamato la print. Questo è possibile perché tutte le classi ereditano dalla classe object (la madre di tutte le classi in Python), che ha un magic method chiamato __repr__, che la print richiama per restituire un valore al nostro programma principale.  

Chi conosce l'ereditarietà sa che una classe figlia può ridefinire la sua classe genitore definendo un proprio metodo con lo stesso nome. 

# Overriding the object __repr__
class Vehicle:
  def __repr__(self):
      return f"{self.__class__.__qualname__}"
​
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

Puoi anche usare il magic method __str__ per creare una rappresentazione testuale richiamata dalla print. 

In effetti, il magic method __repr__ funge da comportamento di riserva per il magic method __str__. Quindi, quando chiamiamo la print, Python cerca prima il metodo __str__ definito nella tua classe e, in mancanza di questo, ricorre al metodo __repr__. 

# Using __str__ for string representation
class Vehicle:
  def __str__(self):
      return f"{self.__class__.__qualname__}"
     
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

La tabella seguente elenca altri magic method relativi alla rappresentazione testuale. 

__str__(self)

Chiamato dalla funzione built-in str() di Python e dall'istruzione print per mostrare la rappresentazione testuale informale di un oggetto. Nota che non è richiesto che sia un'espressione Python valida. 

__repr__(self)

Chiamato dalla funzione built-in repr() di Python e dalle conversioni di stringa per mostrare la rappresentazione testuale ufficiale di un oggetto. Nota che dovrebbe sembrare un'espressione Python valida, ma se non è possibile, è preferibile una stringa con una descrizione utile. 

__unicode__(self)

Chiamato per implementare la funzione built-in unicode() di Python e restituisce un oggetto Unicode. 

__hash__(self)

Chiamato dalla funzione built-in hash() di Python e dalle operazioni sui membri di collezioni hashate. Il metodo deve restituire un intero. 

__nonzero__(self)

Chiamato dall'operazione built-in bool() di Python per implementare il test di veridicità. Il metodo deve restituire True o False (o il loro equivalente intero 1 o 0). 

Metodi "magici" di incapsulamento 

L'incapsulamento è uno dei concetti fondamentali della programmazione orientata agli oggetti (OOP). Si riferisce al raggruppamento dei dati con i metodi che operano su tali dati o alla restrizione dell'accesso diretto a determinati componenti all'interno di un oggetto.

Sviluppatori provenienti da altri linguaggi OOP contestano spesso il fatto che Python non abbracci pienamente l'incapsulamento per le classi. Sono abituati ad avere modificatori espliciti per metodi o campi che permettono di definire, ad esempio, attributi privati con getter e setter pubblici. La differenza in Python è che gran parte dell'incapsulamento avviene tramite i magic method. 

Per esempio, __setattr__ è una soluzione di incapsulamento che ci consente di definire un comportamento per l'assegnazione a un attributo a prescindere dalla sua esistenza. In altre parole, si possono definire regole per i cambiamenti dei valori degli attributi. 

import string

class Alphabet():
    def __setattr__(self, name, value):
        self.__dict__[name] = value.upper()

a = Alphabet()

# Assinging an attribute that does not exist
# The setattr should convert it to caps despite being set as lowercase
a.alphabet = string.ascii_lowercase
print(a.alphabet)

Nel codice sopra, abbiamo creato una classe chiamata Alphabet senza attributi, ma abbiamo detto a Python che volevamo mettere in maiuscolo qualsiasi attributo dell'istanza di classe che impostiamo.

Sappiamo che l'OOP può essere un po' ostica per alcuni lettori. Per metterti in pari con l'OOP in Python, dai un'occhiata al Python Programmer Track di DataCamp.   

Magic method matematici

Un'altra categoria di magic method di Python sono i magic method matematici (noti anche come normali operatori aritmetici). Le espressioni sono create usando simboli speciali che chiamiamo "operatori" in Python. Usare tali operatori in un'espressione implica creare oggetti con magic method matematici. Il mancato rispetto di questa regola solleverà una TypeError. 

class Numbers:
  def __init__(self, a, b):
      self.a = a
      self.b = b
   
set_a = Numbers(2, 4)
set_b = Numbers(3, 5)
​
print(set_a + set_b)
"""
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/3639634723.py in <module>
      7 set_b = Numbers(3, 5)
      8
----> 9 print(set_a + set_b)

TypeError: unsupported operand type(s) for +: 'Numbers' and 'Numbers'
"""

Nel codice sopra, abbiamo provato a creare più istanze della classe Numbers e ad aggiungerle tra loro, cosa che ha generato un errore come previsto. Una soluzione potrebbe essere creare un metodo nella nostra classe per sommare i numeri, ma ciò ci impedirebbe di creare espressioni. 

Una soluzione migliore è implementare un magic method __add__ come segue:  

# Using add operator
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
b = Numbers(3, 5)
print(a + b) # Should result in Numbers(5, 9)


"""
RandomNumbers(5, 9)
"""

L'esempio di codice sopra ci consente solo di sommare due oggetti Numbers. Implementiamo un altro magic method matematico che ci permetta di moltiplicare un oggetto Numbers per un intero. 

# Multiplying Numbers with an integer
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
         
      return Numbers(self.a * other, self.b * other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(a * 5) # Should return 10, 20




"""
RandomNumbers(10, 20)
"""

Nel codice sopra, abbiamo aggiunto un magic method __mul__ per permetterci di moltiplicare i numeri della nostra classe personalizzata per un intero. 

Nota che l'oggetto Numbers compare a sinistra dell'operatore * – non è un caso. Mettiamolo a destra dell'operando e osserviamo cosa succede. 

- snip --

a = Numbers(2, 4)
print(3 * a)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/2857655049.py in <module>
      1 a = Numbers(2, 4)
----> 2 print(3 * a)

TypeError: unsupported operand type(s) for *: 'int' and 'Numbers'

Python solleva una TypeError. 

Il magic method __mul__ che abbiamo implementato copre solo il caso in cui l'oggetto Numbers si trovi sul lato sinistro dell'operatore *. Per estendere questa funzionalità, dobbiamo aggiungere il metodo inverso, __rmul__. 

# Enabling reverse multiplication
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
      return Numbers(self.a * other, self.b * other)

    def __rmul__(self, other):
        return self.__mul__(other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(5 * a) # Should return 10, 20

"""
Numbers(10, 20)
"""

Ora possiamo creare un'espressione di moltiplicazione senza dover considerare da che lato dell'operando compare l'oggetto Numbers. 

Conclusione

Il principio di base per sfruttare i magic method di Python resta lo stesso: creare una classe e sovrascrivere i magic method della classe object. I magic method in Python sono davvero tanti, quindi questo tutorial sarebbe lunghissimo se decidessimo di coprirli tutti singolarmente. Ti consigliamo vivamente di consultare la documentazione di Python per farti un'idea della portata completa dei magic method che puoi usare per migliorare le tue competenze di programmazione in Python.  



Argomenti

Correlato