Metaklasy#
Metaklasą nazywamy obiekt (najczęściej klasę) generujący inne klasy.
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t”
– Python Guru Tim Peters
Klasy jako obiekty#
Podobnie jak w przypadku funkcji, klasy są obiektami. Służą do tworzenia nowych obiektów (instancji).
class MyClass:
    pass
Nowy obiekt jest tworzony przy pomocy operatora (). Jego typ to nazwa klasy.
mc = MyClass()
type(mc)
__main__.MyClass
Jakiego typu jest obiekt klasy?
type(MyClass)
type
Dynamiczne tworzenie klas#
Skoro są obiekty klas są obiektami typu type, to możemy je też dynamicznie tworzyć.
Funkcja type działa też jak fabryka klas, która przyjmuje trzy argumenty:
- nazwa klasy 
- krotka z klasami bazowymi 
- słownik zawierający nazwy atrybutów i ich wartości 
W rezultacie klasę utworzoną w klasyczny sposób:
class Shape:
    def draw(self):
        pass
class Rectangle(Shape):
    _id = 'RECTANGLE'
    def __init__(self, width, height):
        self.width = width
        self.height = height        
    def draw(self):
        print(f'Drawing {Rectangle._id}({self.width}, {self.height})')
możemy utworzyć też dynamicznie:
def rect_init(self, width, height):
        self.width = width
        self.height = height  
RectangleT = type('RectangleT', (Shape, ), {
        '_id': 'RECTANGLE_T',
        '__init__': rect_init,
        'draw' : lambda self: print(f'Drawing {RectangleT._id}({self.width}, {self.height})')
})
RectangleT.__name__
'RectangleT'
type(RectangleT)
type
rect = RectangleT(10, 20)
rect.draw()
Drawing RECTANGLE_T(10, 20)
Metaklasy#
Typ type jest więc wbudowaną w Pythona metaklasą. Jednakże istnieje możliwość stworzenia własnych metaklas.
Pod Pythonem 3, składnia jest następująca:
class MyClass(object, metaclass=class_creator):
    ...
gdzie class_creator to specjalny obiekt, którego należy użyć zamiast type do utworzenia obiektu klasy.
Funkcja jako metaklasa#
W szczególności, metaklasą może być funkcja. Poniżej przedstawiono metaklasę, która konwertuje nazwy wszystkich atrybutów tak, aby używały wielkich liter.
def upper_attr(cls, parents, attrs):
    _attrs = ((name.upper(), value)
                for name, value in attrs.items())
    attrs_upper = dict(_attrs)
    return type(cls, parents, attrs_upper)
class Foo(metaclass=upper_attr):
    bar = 'foo'
foo = Foo()
foo.BAR
'foo'
Klasa metaklasy#
Zazwyczaj jednak metaklasa jest klasą dziedziczącą po type.
class UpperAttr(type):
    def __new__(cls, name, parents, attrs):
        _attrs = ((name.upper(), value)
                  for name, value in attrs.items())
        attrs_upper = dict(_attrs)
        return type(name, parents, attrs_upper)
class Boo(object, metaclass=UpperAttr):
    bar = 'boo'
foo = Foo()
foo.BAR
'foo'
Metody specjalne metaklasy#
from typing import Any, Dict, Mapping, Tuple, Type
class Metaclass(type):
    @classmethod 
    def __prepare__(mcs, name: str, bases: Tuple[Type, ...], **kwargs: Any) -> Mapping[str, Any]: 
        print(f'Metaclass.__prepare__(mcs={mcs},\n'
              f'\tname={name},\n'
              f'\tbases={bases!r},\n'
              f'\tkwargs={kwargs!r})')
        return super().__prepare__(mcs, name, bases, **kwargs)
    
    def __new__(mcs, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any], **kwargs: Any):
        print(f'Metaclass.__new__(mcs={mcs},\n'
              f'\tname={name},\n'
              f'\tbases={bases!r},\n'
              f'\tnamespace={namespace!r},\n'
              f'\tkwargs={kwargs!r})')
        return super().__new__(mcs, name, bases, namespace)
    
    def __init__(cls, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any], **kwargs: Any) -> None: 
        print(f'{cls}.__init__(name={name},\n\tbases={bases!r},\n\tnamespace={namespace!r},\n\tkwargs={kwargs!r})')
        super().__init__(name, bases, namespace, **kwargs) 
    def __call__(cls, *args: Any, **kwargs: Any) -> Any: 
        print(f'{cls}.__call__(args={args!r}, kwargs={kwargs!r})')
        return super().__call__(*args, **kwargs)
class User:
    pass
class SuperUser(User, metaclass=Metaclass, value = 42):
    id: int = 665
    
    def __init__(self, id: int, name: str):
        print(f'{self}.__init__(id={id}, name={name})')
        self.id = id
        self.name = name
    def set_password(self, new_password: str):
        pass
Metaclass.__prepare__(mcs=<class '__main__.Metaclass'>,
	name=SuperUser,
	bases=(<class '__main__.User'>,),
	kwargs={'value': 42})
Metaclass.__new__(mcs=<class '__main__.Metaclass'>,
	name=SuperUser,
	bases=(<class '__main__.User'>,),
	namespace={'__module__': '__main__', '__qualname__': 'SuperUser', '__annotations__': {'id': <class 'int'>}, 'id': 665, '__init__': <function SuperUser.__init__ at 0x7f91006fdd80>, 'set_password': <function SuperUser.set_password at 0x7f91006fde10>},
	kwargs={'value': 42})
<class '__main__.SuperUser'>.__init__(name=SuperUser,
	bases=(<class '__main__.User'>,),
	namespace={'__module__': '__main__', '__qualname__': 'SuperUser', '__annotations__': {'id': <class 'int'>}, 'id': 665, '__init__': <function SuperUser.__init__ at 0x7f91006fdd80>, 'set_password': <function SuperUser.set_password at 0x7f91006fde10>},
	kwargs={'value': 42})
user = SuperUser(667, "admin")
<class '__main__.SuperUser'>.__call__(args=(667, 'admin'), kwargs={})
<__main__.SuperUser object at 0x7f9100723850>.__init__(id=667, name=admin)
Metoda specjalna __prepare__#
Zadaniem tej metody jest zwrócenie słownika, który zostanie wykorzystany do zainicjowania obiektu __dict__ w tworzonym obiekcie typu (klasy). Domyślna implementacja zwraca pusty słownik (typu dict), ale można to zmienić (np. zwrócić wstępnie wypełnioną instancję słownika).
Metoda specjalna __new__#
Jest odpowiedzialna za utworzenie obiektu klasy.
Dostaje jako argument wywołania obiekt metaklasy, nazwę tworzonego typu (klasy), krotkę klas bazowych i wypełniony słownik z atrybutami. Możliwa jest modyfikacja tych parametrów przed przekazaniem ich (najczęściej) w wywołaniu __new__() z klasy bazowej, czyli type.__new__()
Metoda specjalna __init__#
Dostaje jako argument wywołania obiekt utworzonej już klasy, z wypełnionym słownikiem atrybutów.
Metoda specjalna __call__#
Metoda wywoływana, gdy tworzona jest instancja docelowej klasy (utworzonej za pomocą danej metaklasy).
Domyślna implementacja z type wywołuje operacje|:
- __new__(cls, *args, **kwargs)- utworzenie instancji klasy
- __init__(self, *args, **kwargs)- inicjalizacja instancji klasy
Ta implementacja może zostać zmieniona w celu lepszej kontroli sposobu tworzenia instancji klasy.
Zastosowanie metaklas#
W praktyce, metaklasy są stosowane tam, gdzie API klasy musi być tworzone dynamicznie (np. ORM w Django) oraz do implementacji niektórych wzorców projektowych.
Singleton i metaklasa#
class Singleton(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
    
class AsSingleton(metaclass=Singleton):
    pass
class Logger(AsSingleton):
    def __init__(self):
        print(f'Executing Logger.__init__({self})')
    
    def log(self, msg: str) -> None:
        print(f">>{msg}")
logger1 = Logger()
logger2 = Logger()
logger1 is logger2
Executing Logger.__init__(<__main__.Logger object at 0x7f9100722440>)
True
Modyfikacja nazw atrybutów w klasie#
from typing import Any, Mapping
import inflection
class CaseInterpolationDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        super().__setitem__(inflection.underscore(key), value)
class CaseInterpolatedMeta(type):
    @classmethod
    def __prepare__(mcs, __name: str, __bases: Tuple[type, ...], **kwds: Any) -> Mapping[str, object]:
        return CaseInterpolationDict()
    
class MyUser(metaclass=CaseInterpolatedMeta):
    pass
class User(MyUser):    
    def __init__(self, firstName: str, lastName: str):
        self.firstName = firstName
        self.lastName = lastName
    def getDisplayName(self):
        return f"{self.firstName} {self.lastName}"
    
    def greetUser(self):
        return f"Hello {self.getDisplayName()}!"
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[19], line 2
      1 from typing import Any, Mapping
----> 2 import inflection
      4 class CaseInterpolationDict(dict):
      5     def __setitem__(self, key, value):
ModuleNotFoundError: No module named 'inflection'
User.__dict__
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.User.__init__(self, firstName: str, lastName: str)>,
              'getDisplayName': <function __main__.User.getDisplayName(self)>,
              'get_display_name': <function __main__.User.getDisplayName(self)>,
              'greetUser': <function __main__.User.greetUser(self)>,
              'greet_user': <function __main__.User.greetUser(self)>,
              '__doc__': None})
user = User("John", "Doe")
user.getDisplayName()
user.get_display_name()
'John Doe'
