Python avanzato Metaclassi
Python avanzato Metaclassi di livello avanzato
Una breve introduzione a Python class object e come viene creato.
![Come Atlante è per i cieli, le metaclassi sono per le classi. Foto di Alexander Nikitenko su Unsplash](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*0KXEJpkuPM0f8Swd9_gJjw.jpeg)
Questo articolo continua la serie Advanced Python (precedente articolo sulle funzioni in Python). Questa volta, copro una introduzione alle metaclassi. Il tema è piuttosto avanzato perché raramente c’è bisogno per l’ingegnere di implementare metaclassi personalizzate. Tuttavia, è una delle costruzioni e meccanismi più importanti che ogni sviluppatore Python competente dovrebbe conoscere, principalmente perché consente il paradigma OOP.
Dopo aver compreso l’idea dietro le metaclassi e come vengono create le classi, sarai in grado di continuare ad apprendere i principi OOP di encapsulation, abstraction, inheritance e polymorphism. Sarai quindi in grado di capire come applicare tutto ciò attraverso numerosi pattern di design guidati da alcuni dei principi dell’ingegneria del software (ad esempio, SOLID).
Ora, cominciamo con questo esempio apparentemente banale:
class Persona: passclass Bambino(Persona): passbambino = Bambino()
Quando hai imparato la programmazione orientata agli oggetti, probabilmente sei incappato in un’idea generale che descrive cosa sono classi e oggetti, ed è così:
- Creare un modello di rilevamento dell’autunno utilizzando l’apprendimento non supervisionato
- Ricercatori cinesi svelano ImageReward un rivoluzionario approccio all’intelligenza artificiale per ottimizzare i modelli di testo-immagine utilizzando il feedback delle preferenze umane.
- NVIDIA sta abusando della sua dominanza nel mercato dell’IA? Sondaggi dell’UE sul controllo schiacciante di NVIDIA nel settore dei chip per l’IA
“Una classe è come uno stampo per biscotti. E gli oggetti sono biscotti modellati da esso”.
Questa è una spiegazione molto intuitiva e trasmette chiaramente l’idea. Detto questo, il nostro esempio definisce due modelli con poche o nessuna funzionalità, ma funzionano. Puoi sperimentare definendo il metodo __init__
, impostare alcuni attributi dell’oggetto e renderlo più utilizzabile.
Tuttavia, ciò che è interessante in Python è che anche se una classe è un “template” che viene utilizzato per creare oggetti da essa, è anche un oggetto stesso. Tutti coloro che apprendono l’OOP in Python passeranno velocemente su questa affermazione, senza pensarci realmente in profondità. Tutto in Python è un oggetto, e quindi? Ma una volta che inizi a riflettere su questo, sorgono molte domande e si svelano interessanti peculiarità di Python.
Prima di iniziare a porsi queste domande per te, ricordiamoci che in Python, tutto è un oggetto. E intendo tutto. Questo è probabilmente qualcosa che hai già intuito, anche se sei un principiante. L’esempio successivo lo dimostra:
class Persona: passid(Persona) # qualche posizione di memoriaclass Bambino(Persona): passid(Bambino)# qualche posizione di memoria# Gli oggetti di classe vengono creati, puoi istanziare oggettibambino = Bambino()id(bambino)# qualche posizione di memoria
Sulla base di questi esempi, ecco alcune domande che dovresti porsi:
- Se una classe è un oggetto, quando viene creata?
- Chi crea gli oggetti di classe?
- Se una classe è un oggetto, come mai riesco a chiamarla quando istanzio un oggetto?
Creazione di oggetti di classe
Python è ampiamente conosciuto come un linguaggio interpretato. Ciò significa che c’è un interprete (programma o processo) che va riga per riga e cerca di tradurlo in codice macchina. Questo è opposto ai linguaggi di programmazione compilati come C, dove il codice di programmazione viene tradotto in codice macchina prima di eseguirlo. Questa è una visione molto semplificata. Per essere più precisi, Python è sia compilato che interpretato, ma questa è un argomento per un’altra volta. Quello che è importante per il nostro esempio è che l’interprete scorre la definizione della classe e una volta che il blocco di codice della classe è terminato, viene creata l’oggetto di classe. Da quel momento in poi, puoi istanziare degli oggetti da esso. Devi farlo esplicitamente, ovviamente, anche se gli oggetti di classe vengono istanziati implicitamente.
Ma quale “processo” viene innescato quando l’interprete finisce di leggere il blocco di codice della classe? Potremmo andare direttamente ai dettagli, ma un grafico parla più di mille parole:
![Come oggetti, classi e metaclassi sono correlati tra loro. Immagine di Ilija Lazarevic.](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*taygcXPnc-F4iHJ1sxQYeQ.png)
Se non lo sai, Python ha delle funzioni type
che possono essere utilizzate per i nostri scopi ora. Chiamando type
con l’oggetto come argomento, otterrai il tipo dell’oggetto. Che genialità! Dai un’occhiata:
class Persona: passclass Bambino(Persona): passbambino = Bambino()type(bambino)# Bambinotype(Bambino)# type
La chiamata a type
nell’esempio ha senso. bambino
è di tipo Bambino
. Abbiamo usato un oggetto di classe per crearlo. Quindi in qualche modo, puoi pensare a type(bambino)
come al nome del suo “creatore”. E in un certo senso, la classe Bambino
è il suo creatore perché l’hai chiamata per creare una nuova istanza. Ma cosa succede quando provi a ottenere il “creatore” dell’oggetto di classe, type(Bambino)
? Ottieni il type
. Riassumendo, un oggetto è un’istanza di una classe e una classe è un’istanza di un tipo. A questo punto, potresti chiederti come una classe possa essere un’istanza di una funzione, e la risposta è che type
è sia una funzione che una classe. Questo è stato intenzionalmente lasciato così per garantire la compatibilità con le versioni precedenti.
Ciò che farà girare la testa è il nome che abbiamo per la classe usata per creare un oggetto di classe. Si chiama metaclassa. Ed è importante fare una distinzione tra l’ereditarietà dal punto di vista del paradigma orientato agli oggetti e i meccanismi di un linguaggio che ti consentono di praticare tale paradigma. Le metaclassi forniscono questo meccanismo. Ciò che può essere ancora più confuso è che le metaclassi possono ereditare classi genitori proprio come le classi regolari possono fare. Ma questo può diventare rapidamente una programmazione “in inception”, quindi non approfondiamolo.
Dobbiamo occuparci di queste metaclassi quotidianamente? Beh, no. Nei casi rari, probabilmente avrai bisogno di definirle e usarle, ma nella maggior parte dei casi, il comportamento predefinito va benissimo.
Continuiamo il nostro percorso, questa volta con un nuovo esempio:
class Genitore: def __init__(self, nome, eta): self.nome = nome self.eta = etagenitore = Genitore('John', 35)
Qualcosa del genere dovrebbe essere il tuo primo passo OOP in Python. Ti viene insegnato che __init__
è un costruttore in cui imposti i valori degli attributi dell’oggetto e sei pronto. Tuttavia, questo metodo dunder __init__
è esattamente ciò che dice: il passo di inizializzazione. Non è strano che lo chiami per inizializzare un oggetto eppure ottieni un’istanza di un’oggetto in cambio? Non c’è return
lì, giusto? Quindi, come è possibile? Chi restituisce l’istanza di una classe?
Molto pochi apprendono all’inizio del loro percorso Python che c’è un altro metodo che viene chiamato implicitamente e si chiama __new__
. Questo metodo crea effettivamente un’istanza prima che __init__
venga chiamato per inizializzarla. Ecco un esempio:
class Genitore: def __new__(cls, nome, eta): print('viene chiamato il new') return super().__new__(cls) def __init__(self, nome, eta): print('viene chiamato l\'init') self.nome = nome self.eta = etagenitore = Genitore('John', 35)# viene chiamato il new# viene chiamato l'init
Quello che vedrai immediatamente è che __new__
restituisce super().__new__(cls)
. Questa è una nuova istanza. super()
recupera la classe genitore di Genitore,
che è implicitamente una classe object
. Questa classe viene ereditata da tutte le classi in Python. Ed è anche un oggetto di per sé. Un’altra mossa innovativa dei creatori di Python!
isinstance (oggetto, oggetto) # True
Ma cosa lega __new__
e __init__
? Deve esserci qualcosa di più su come viene eseguita l’istanza degli oggetti quando chiamiamo Parent('John', 35)
. Guardateci ancora una volta. Stai invocando (chiamando) un oggetto di classe, come una funzione.
Callable di Python
Python, essendo un linguaggio tipizzato strutturalmente, consente di definire metodi specifici nella tua classe che descrivono un Protocollo (un modo di usare il suo oggetto) e in base a questo, tutte le istanze di una classe si comporteranno nel modo previsto. Non intimidirti se provieni da altri linguaggi di programmazione. I protocolli sono qualcosa di simile alle interfacce in altri linguaggi. Tuttavia, qui non dichiariamo esplicitamente che stiamo implementando un’interfaccia specifica e, quindi, un comportamento specifico. Implementiamo solo i metodi che sono descritti dal Protocollo, e tutti gli oggetti avranno il comportamento del protocollo. Uno di questi protocolli è Callable. Implementando __call__
come metodo speciale, consenti al tuo oggetto di essere chiamato come una funzione. Guarda l’esempio:
class Parent: def __new__ (cls, nome, età): print ('nuovo è chiamato') return super (). __new__ (cls) def __init __ (self, nome, età): print ('init è chiamato') self.nome = nome self.età = età def __call __ (self): print ('Genitore qui! ') parent = Parent ('John', 35) parent () # Parent here!
Implementando __call__
nella definizione della classe, le istanze della tua classe diventano chiamabili. Ma cosa succede con Parent ('John', 35)
? Come puoi ottenere lo stesso risultato con il tuo oggetto di classe? Se la definizione del tipo di oggetto (classe) specifica che l’oggetto è chiamabile, allora il tipo di oggetto della classe (tipo cioè metaclass) dovrebbe specificare che l’oggetto di classe è chiamabile anche, giusto? L’invocazione dei metodi dunder __new__
e __init__
avviene lì.
A questo punto, è il momento di iniziare a giocare con le metaclassi.
Metaclassi Python
Esistono almeno due modi per modificare il processo di creazione dell’oggetto di classe. Uno è utilizzando i decoratori di classe; l’altro è specificando esplicitamente una metalcass. Descriverò l’approccio delle metaclassi. Tieni presente che una metaclass sembra una classe normale e l’unico eccezione è che deve ereditare una classe di tipo. Perché? Perché le classi di tipo hanno tutta l’implementazione necessaria affinché il nostro codice funzioni ancora come previsto. Ad esempio:
class MyMeta (type): def __call __ (self, * args, ** kwargs): print (f' {self.__name__} è chiamato ' f'con args = {args}, kwargs = {kwarg}') class Genitore (metaclass = MyMeta): def __new __ (cls, nome, età): print ('nuovo è chiamato') return super (). __new__ ( cls) def __init __ (self, nome, età): print ('init è chiamato') self.nome = nome self.età = età parent = Parent ('John', 35) # Parent is called with args = ('John', 35), kwargs = {} type (parent) # NoneType
Qui, MyMeta
è la forza trainante dietro la creazione di un nuovo oggetto di classe e specifica anche come vengono creati nuovi istanze della classe. Dai un’occhiata più da vicino alle ultime due righe dell’esempio. parent
non contiene nulla! Ma perché? Perché, come puoi vedere, MyMeta.__call__
stampa solo informazioni e non restituisce nulla. Esplicitamente, intendo. Implicitamente, questo significa che restituisce None
, che è del tipo NoneType
.
Come possiamo risolvere questo?
class MyMeta (type): def __call __ (cls, * args, ** kwargs): print (f' {cls.__name__} è chiamato ' f'con args = {args}, kwargs = {kwargs}') print ('metaciclo chiama __new__') obj = cls.__new__ (cls, * args, ** kwargs) if isinstance (obj, cls): print ('metaciclo chiama __init__') cls.__init__ (obj, * args, ** kwargs) return obj class Parent (metaclass = MyMeta): def __new__ (cls, nome, età): print ('nuovo è chiamato') return super (). __new__ (cls) def __init__ (self, nome, età): print ('init è chiamato') self.nome = nome self.età = età parent = Parent ('John', 35) # Parent is called with args = ('John', 35), kwargs = {} # metaciclo chiamate __new__ # nuovo è chiamato # metaciclo chiama __init__ # init è chiamato type (parent) # Parentstr (parent) # '<__main__.Parent oggetto a 0x103d540a0> '
Dall’output, puoi vedere cosa succede all’invocazione di MyMeta.__call__
. L’implementazione fornita è solo un esempio di come funziona l’intero processo. Dovresti fare attenzione se hai intenzione di ridefinire parti di metaclassi tu stesso. Ci sono alcuni casi limite che devi considerare. Ad esempio, uno dei casi limite è che Parent.__new__
può restituire un oggetto che non è un’istanza della classe Parent
. In quel caso, non verrà inizializzato dal metodo Parent.__init__
. Questo è il comportamento atteso di cui devi essere consapevole e non ha senso inizializzare un oggetto che non è un’istanza della stessa classe.
Conclusioni
Questo concluderebbe una breve panoramica di cosa succede quando si definisce una classe e si crea un’istanza di essa. Naturalmente, potresti andare ancora oltre e vedere cosa succede durante l’interpretazione del blocco della classe. Tutto questo succede anche nella metaclass. È fortunato per la maggior parte di noi che probabilmente non avremo bisogno di creare e utilizzare metaclassi specifici. Tuttavia, è utile capire come funziona tutto. Mi piacerebbe usare un detto simile che si applica all’uso dei database NoSQL, che dice più o meno così: se non sei sicuro se devi utilizzare le metaclassi Python, probabilmente non devi farlo.