lettura facile

Il metodo __new__ in Python

In Python il metodo `__new__()` viene chiamato automaticamente ogni volta che crei una nuova istanza di una classe. E' anche detto costruttore.

Questo metodo è raramente sovrascritto, ma può essere molto utile in situazioni particolari, ad esempio quando vuoi controllare il processo di creazione degli oggetti.

Quando crei un nuovo oggetto, Python chiama i metodi in questo ordine:

  1. il metodo `__call__()`
  2. il metodo `__new__()`
  3. il metodo `__init__()`

Quindi, il metodo `__new__()` è il primo vero passo nella creazione di un oggetto perché viene chiamato prima di `__init__()`.

Quali sono le differenze?Il metodo `__new__()` è responsabile della creazione vera e propria dell'oggetto. Prende come argomento la classe stessa come primo parametro, solitamente chiamato `cls`, e restituisce una nuova istanza della classe. Il metodo `__init__()`, invece, viene chiamato subito dopo e si occupa di inizializzare l'oggetto che è stato appena creato. Prende come primo argomento l'istanza dell'oggetto (solitamente chiamata `self`).

Vediamo un esempio pratico per chiarire meglio.

Definisci una classe "MyClass"

class MyClass:
    def __new__(cls, *args, **kwargs):
      print("Chiamato __new__")
      instance = super(MyClass, cls).__new__(cls)
      return instance

    def __init__(self, value):
      print("Chiamato __init__")
      self.value = value

Nella classe sono definiti sia il metodo __new__() che il metodo __init__()

Ora crea un'istanza della classe "MyClass"

obj = MyClass(10)

Nel processo di creazione dell'istanza Python esegue prima il metodo __new__() per creare un nuovo oggetto e poi il metodo __init__() per inizializzarlo

Ecco il risultato in output

Chiamato __new__
Chiamato __init__

Nota che all''interno di `__new__()` abbiamo usato `super()` per chiamare il metodo `__new__()` della classe base, che è necessario per creare l'oggetto.

Se non delegassimo la costruzione della nuova istanza alla classe base, si verificherebbe una ricorsione infinita.

Cosa accade se la classe non ha il metodo `__new__()`?

Se in una classe Python non viene definito un metodo __new__(), Python utilizza automaticamente il metodo __new__() della classe base da cui la classe in questione eredita.

Per la maggior parte delle classi, questa classe base è la classe object, la superclasse fondamentale in Python da cui derivano tutte le altre classi.

Il metodo __new__() della classe object si occupa di creare e restituire una nuova istanza della classe.

In altre parole, quando definisci il metodo __new__() in una classe stai facendo l'overriding, ossia la sovrascrittura, di un metodo che viene comunque ereditato dalla classe base. In Python tutte le classi derivano dalla classe object in cui è definito il metodo costruttore __new__(). Pertanto, nel linguaggio Python tutte le classi implementano automaticamente il metodo __new__().

Questo è il comportamento predefinito e funziona per la maggior parte dei casi.

È per questo motivo che nella stragrande maggioranza delle classi Python non è necessario definire esplicitamente un metodo __new__().

Quando è utile sovrascrivere `__new__()`?

In generale, è raro sovrascrivere il metodo __new__(). Tuttavia, possono capitare dei casi in cui è utile farlo.

Ad esempio, sovrascrivere `__new__()` è utile se vuoi assicurarti che solo una singola istanza di una classe venga creata o quando devi controllare o modificare l'oggetto prima che venga inizializzato.

class Singleton:
    _instance = None
    _initialized = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

    def __init__(self, a=None, b=None):
        if not self._initialized:
            self.a = a
            self.b = b
            self._initialized = True

In questo codice puoi creare un'istanza della classe e iniziarla una sola volta, grazie all'override del metodo __new__ e del metodo __init__.

s1 = Singleton(1, 2)

Se provi a creare una seconda istanza della classe, viene generato un riferimento alla prima istanza.

s2 = Singleton(3, 4)
print(s2.a, s2.b)

1 2

In altre parole, non viene creato un nuovo oggetto perché s1 e s2 sono etichette che si riferiscono allo stesso oggetto, ovvero alla prima istanza

print(s1 is s2)

True

L'overriding del metodo __new__  ti permette anche di creare un nuovo tipo di dato ereditando i metodi e le proprietà di un tipo predefinito.

Ad esempio, una tupla è un tipo immutabile perché una volta definita non puoi modificarne il contenuto.

Supponiamo di voler creare una tupla che accetta solo valori positivi. Per farlo puoi creare un nuovo tipo di tupla che eredita i metodi delle tuple.

class PositiveTuple(tuple):
    def __new__(cls, seq):
        # Verifica se tutti gli elementi della sequenza sono positivi
        if not all(x > 0 for x in seq):
            raise ValueError("All elements must be positive numbers.")
        
        # Crea la tupla utilizzando solo i valori positivi
        return super().__new__(cls, seq)

L'overriding del metodo __new__ aggiunge la condizione che tutti gli elementi della tupla siano positivi.

Ad esempio, per creare una tupla del nuovo tipo puoi scrivere

pt = PositiveTuple((1, 2, 3, 4))

Poiché la classe eredita tutti i metodi delle tuple, puoi continuare a usarli anche sul nuovo tipo PositiveTuple.

Se invece provi a inserire un valore nullo o negativo, la classe solleva un'eccezione e impedisce la creazione della nuova tupla.

pt = PositiveTuple((1, -2, 3, 4))

ValueError: All elements must be positive numbers.

In questo modo puoi creare dei tipi personalizzati ereditando le proprietà e i metodi dei tipi predefiniti.

Nota che i tipi immutabili, come le tuple, non possono essere modificati dopo averli creati, quindi è sufficiente l'overriding solo del metodo __new__. Nel caso dei tipi mutabili, come le liste, è necessario l'overriding anche del metodo __init__ e di altri metodi che ti permettono di modificare i dati come __append__, __insert__, __extend__, ecc.

Quindi, anche se nella maggior parte dei casi non è necessario sovrascrivere il metodo __new__, in alcune situazioni avanzate può fare la differenza.

Conoscere come funziona ti dà una comprensione più profonda di come Python gestisce la creazione degli oggetti.

Spero che questa spiegazione ti sia stata utile!




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




FacebookTwitterLinkedinLinkedin