--- post kind: guide language: it-IT --- > [!Info] > Questa è una [[guida]] da pubblicare sul mio sito, [[steffo.eu]]. > > ## #TODO > - [x] Proofreading (Zelda) > - [x] Cosa sono le `bases`? Cos'è il `namespace`? > - [ ] Proofreading # Metaclassi in Python In [[Python]], *tutto* è un oggetto, incluse le classi stesse. Infatti, possiamo vedere che le classi sono istanze della classe speciale `type`: ```python class Something: pass assert isinstance(Something, type) ``` ## Quando non bastano i `@classmethod` Come veri e propri oggetti, le classi possono avere attributi e in generale comportarsi da oggetti loro stesse, e anche avere propri metodi attraverso il decoratore `@classmethod`: ```python class Something: abc: int = 123 @classmethod def get_abc(cls) -> int: return cls.abc assert Something.abc == 123 assert Something.get_abc() == 123 assert str(Something) == "" assert type(Something) == type ``` Non sempre però questo basta per ottenere le funzionalità desiderate. Ad esempio, `@classmethod` non è sufficiente per permettere a due classi di essere sommate tra di loro con l'operatore `+` (metodo `__add__`) senza crearne un'istanza: ```python class Number: value: int = 0 @classmethod def get_value(cls) -> int: return self.cls @classmethod # Non funziona! def __add__(cls, other): if issubclass(other, Number): return cls.value + other.value else: return cls.value + other class One(Number): value = 1 class Two(Number): value = 2 assert One.get_value() == 1 assert Two.get_value() == 2 assert One + Two == 3 # Traceback (most recent call last): #  File "", ... #    assert One + Two == 3 #           ~~~~^~~~~ # TypeError: unsupported operand type(s) for +: 'type' and 'type' ``` In tal caso, dobbiamo andare a modificare il *tipo della classe stessa*, e poi usare *quel nuovo tipo* per creare la classe. Possiamo farlo creando una nuova classe che eredita da `type`, definendoci i metodi desiderati sopra come metodi di istanza, e creando le nuove classi specificando il parametro `metaclass=...` dove specificheremmo l'ereditarietà: ```python class Number(type): # Entrambi i metodi non hanno @classmethod # Vengono chiamati sulle *istanze* di questo tipo # Ovvero le *classi create con esso* def get_value(self) -> int: return self.value def __add__(self, other): if isinstance(other, Number): # Cambiato issubclass a isinstance return self.value + other.value else: return self.value + other class One(metaclass=Number): value = 1 class Two(metaclass=Number): value = 2 assert One.get_value() == 1 assert Two.get_value() == 2 assert One + Two == 3 assert type(One) == Number assert type(Number) == type ``` ## Modificare `__init__` di una metaclasse Con questo sistema possiamo quindi modificare i metodi speciali (doppio underscore) delle classi stesse. E questo include `__init__`! Ad esso, vengono automaticamente passati tre parametri, di cui possiamo fare uso nella chiamata: - `name`, una `str` contenente il nome della classe creata; - `bases`, una `tuple` contenente tutte le classi da cui quella creata eredita; - `namespace`, un `dict` che associa i nomi degli attributi di classe ai loro valori, un po' come fa `locals()` con le variabili locali. Ad esempio, possiamo fare in modo che, quando viene creata una classe usando la nostra metaclasse, venga stampato un messaggio contenente le tutte le sue informazioni: ```python from typing import Any class NotifyType(type): def __init__( self, name: str, bases: tuple[type, ...], namespace: dict[str, Any], ): super().__init__(name, bases, namespace) print(f"A new class was just created!") print(f"Its name is: {name!r}") print(f"It inherits from: {bases!r}") print(f"It defines these attributes and methods: {namespace!r}") class Example(metaclass=NotifyType): VARIABLE = 1234 ##### OUTPUT ##### # A new class was just created! # Its name is: 'Example' # It inherits from: () # It defines these attributes and methods: {'__module__': '__main__', '__qualname__': 'Example', '__firstlineno__': 16, 'VARIABLE': 1234, '__static_attributes__': ()} ##### FINE ##### ``` Essendo la classe già stata creata quando viene chiamato `__init__`, non possiamo però modificare quei parametri, solo farne uso. ## Modificare `__new__` di una metaclasse Per modificare quei parametri, dobbiamo agire un passo prima, prima che la classe venga creata. Abbiamo visto che creare una classe corrisponde a *creare un'istanza della sua metaclasse*, e ci ricordiamo che, per tutti i tipi di Python, la creazione delle istanze di una classe avviene nel metodo `__new__`. Possiamo allora cambiare *come funziona definire una nuova classe* modificando il metodo `__new__` della sua metaclasse! > [!Hint]- Funzionamento generale di `__new__` in Python > > In tutte le classi di Python (non solo le metaclassi) `__new__` è un metodo che: > > - riceve gli stessi parametri che vengono passati ad `__init__`, più un parametro iniziale `cls` che corrisponde alla classe a cui appartiene il metodo, come se fosse un `@classmethod`; > > - restituisce il valore che sarà considerato il risultato del costruttore; > > - determina se deve essere successivamente chiamato `__init__`: se il valore restituito non è un'istanza di `cls`, `__init__` non viene chiamato. > > È molto raro che sia necessario sovrascriverlo: ciò solitamente succede quando si vuole modificare il comportamento di built-in di Python, come, nel caso delle metaclassi, il built-in `type`. Ad esempio, possiamo fare in modo che una classe erediti automaticamente dall'ultima classe che è stata creata, realizzando così una catena "implicita" di classi: ```python from typing import Self, Any class ChainType(type): previously_created_class = None def __new__( cls: type[Self], name: str, bases: tuple[type, ...], namespace: dict[str, Any], ) -> Self: # Aggiungiamo l'ultima classe creata a `bases` if cls.previously_created_class != None: bases = (*bases, cls.previously_created_class) # Creiamo effettivamente la classe con `bases` modificato instance = super().__new__(cls, name, bases, namespace) # Aggiorniamo l'ultima classe creata cls.previously_created_class = instance # Restituiamo la classe creata return instance class A(metaclass=ChainType): is_letter = True class B(metaclass=ChainType): pass class C(metaclass=ChainType): pass assert issubclass(B, A) assert issubclass(C, B) assert issubclass(C, A) assert A.is_letter is True assert B.is_letter is True assert C.is_letter is True ``` Oppure, se vogliamo confondere un nostro collega non creando proprio la classe che ha definito: ```python class Lol(type): def __new__(cls, name, bases, namespace): return None class Haha(metaclass=Lol): pass assert Haha is None ``` ## Passare keyword arguments a `__new__` Per funzionalità più avanzate, possiamo specificare dopo `metaclass=...` una serie di keyword arguments che verranno passati a `__new__`. Ad esempio, possiamo fare in modo che vengano automaticamente definiti tanti attributi di classe con uno specifico valore: ```python from typing import Iterable, Any class Definer(type): def __new__(cls, name, bases, namespace, *, keys: Iterable[str], value: Any): for key in keys: namespace[key] = value return super().__new__(cls, name, bases, namespace) class OopsAllSixes( metaclass=Definer, keys=("A", "B", "C", "D", "E", "F"), value=6, ): pass assert OopsAllSixes.A == 6 assert OopsAllSixes.B == 6 assert OopsAllSixes.C == 6 assert OopsAllSixes.D == 6 assert OopsAllSixes.E == 6 assert OopsAllSixes.F == 6 ``` ## Chiamare manualmente il costruttore delle metaclassi Possiamo accorgerci che possiamo chiamare il metodo `__new__` e `__init__` di una metaclasse come un normale costruttore di un'istanza, creando però una nuova classe. Ovviamente, è necessario fornirgli i parametri appropriati: ```python class Meta(type): def __new__(cls, name, bases, namespace): return super().__new__(cls, name, bases, namespace) Class = Meta("Class", (), {"HAD_ICECREAM": True}) assert isinstance(Class, Meta) assert Class.HAD_ICECREAM is True ``` Possiamo intuire allora che la definizione di una classe è solo una sintassi alternativa per questa chiamata: ```python class Meta(type): def __new__(cls, name, bases, namespace): return super().__new__(cls, name, bases, namespace) class Class(metaclass=Meta): HAD_ICECREAM = True ``` Visto che tutte le metaclassi ereditano da `type` stessa, anch'essa deve supportare la stessa sintassi: ```python Class = type("Class", (), {"HAD_PIZZA": True}) assert isinstance(Class, type) assert Class.HAD_PIZZA is True ``` E allora possiamo capire che la metaclasse "di default" di tutte le classi è `type`: ```python class Class(metaclass=type): pass ``` ## Hai probabilmente usato metaclassi senza saperlo Due casi molto comuni si usano metaclassi sono: - quando si dichiarano [enum](https://docs.python.org/3/library/enum.html) (`Enum`), il cui tipo è definito con la metaclasse [`EnumType`](https://docs.python.org/3/library/enum.html#enum.EnumType), che modifica come vengono interpretati gli attributi della classe definita; - quando si dichiarano [abstract base classes](https://docs.python.org/3/library/abc.html) (`ABC`), il cui tipo è definito con la metaclasse [`ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta), che verifica al momento di creazione di una nuova classe che non siano rimasti metodi taggati con `@abstractmethod`. Solitamente, però, non lo si nota: quando si eredita da una classe che usa una certa metaclasse, è implicito che anche la nuova classe farà uso di quella metaclasse: ```python from enum import Enum, EnumType, auto class Fruit(Enum): APPLE = auto() PEAR = auto() BANANA = auto() assert isinstance(Fruit, EnumType) ``` ## Conflitto di metaclassi Questa relazione implicita però diventa un problema nel momento in cui l'ereditarietà diventa multipla, ad esempio se si cerca di creare un enum astratto, perchè non è più possibile determinare quale metaclasse deve essere usata per creare la classe: ```python from enum import Enum from abc import ABC, abstractmethod class AbstractEnum(ABC, Enum): @abstractmethod def print_something(self): raise NotImplementedError() # Cosa deve chiamare l'interprete per creare la classe? # AbstractEnum = ABC("AbstractEnum", ...) # oppure # AbstractEnum = EnumType("AbstractEnum", ...) # ? # # Traceback (most recent call last): #  File "", ... #    class AbstractEnum(ABC, Enum): #    ...<2 lines>... #            raise NotImplementedError() # TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases ``` Una possibile soluzione è quella di creare una propria *metaclasse* che erediti da entrambe *le metaclassi* e faccia quello che si desidera: ```python from enum import EnumType from abc import ABCMeta, abstractmethod # Quale dei due __new__ chiamo prima? Quello di ABCMeta, o quello di EnumType? class AbstractEnumType(ABCMeta, EnumType): ... class EnumAbstractType(EnumType, ABCMeta): ... ```