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'