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ą obiektami typu type, to możemy je dynamicznie tworzyć:

def choose_class(system: str):
    if system == "windows":
        class WindowsSpecificClass:
            pass
        return WindowsSpecificClass
    else:
        class DefaultClass:
            pass
        return DefaultClass
    
os_class = choose_class("windows")

os_class
__main__.choose_class.<locals>.WindowsSpecificClass

Skoro klasa jest typu type, to może można użyć go tak jak klasy?

type przyjmuje trzy argumenty:

  • nazwa klasy

  • krotka z rodzicami klasy

  • słownik zawierający nazwy atrybutów i ich wartości

attrs = {'foo': "foo_value", 'bar': 123}

DynamicClass = type("DynamicClass", (), attrs)

dc = DynamicClass()
dc.bar
123

Dziedziczenie i metaklasy

Używając instrukcji class można zdefiniować klasę dziedziczącą po innej klasie w następujący sposób:

class MyClassChild(DynamicClass):
    bar = 665

child_mc = MyClassChild()
child_mc.bar
665
child_mc.foo
'foo_value'

Jeżeli chcemy stworzyć klasę dynamicznie (z użyciem type), to jako drugi argument możemy przekazać krotkę rodziców:

DynamicClassChild = type("DynamicClassChild", (DynamicClass,), {'bar': 65})
child_dc = DynamicClassChild()
child_dc.bar
65
child_dc.foo
'foo_value'

Metody klasy

Metody też mogą stanowić część słownika przekazywaną do type:

def echo(self):
    print(f"Echo: {self.foo}")

AnotherDynamicClass = type("AnotherDynamicClass", (DynamicClass,), {'bar': 42, 'echo' : echo})

acd = AnotherDynamicClass()
acd.echo()
Echo: foo_value

Metaklasy

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'

Przykład metaklasy

Zazwyczaj jednak metaklasa jest klasą.

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'

Metoda specjalna __new__

__new__(cls, ...) jest metodą wywoływaną, aby utworzyć nową instancję obiektu. Przekazywany jest do niej obiekt klasy oraz argumenty konstruktora.

Dla porównania, __init__(self, ...) jest konstruktorem i jest wykonywany, gdy inicjalizowany jest obiekt (instancja). Przekazywany jest do niego obiekt instancji.

Szablon metaklasy

Ponieważ metaklasy są tak naprawdę zwykłymi klasami, to mogą podlegać dziedziczeniu. Aby zapewnić bezproblemowe dziedziczenie, musimy użyć super():

class UpperAttrChild(type):
    def __new__(cls, name, parents, attrs):
        _attrs = ((name.upper(), value) for name, value in attrs.items())
        attrs_upper = dict(_attrs)
        return super(UpperAttrChild, cls).__new__(cls, name, parents, attrs_upper)

__call__ metaklasy

Ciekawym aspektem jest użycie metody __call__. Jest ona wywoływana dla gotowego obiektu klasy wtedy, gdy używamy “wywołania” (operatora ())

class Meta(type):
    def __call__(cls, *args, **kwargs):
        print('__call__ of ', str(cls))
        print('__call__ *args=', str(args))
        return type.__call__(cls, *args, **kwargs)
class Gadget(metaclass=Meta):
    def __init__(self, name, price):
        self.name = name
        self.price = price
        print(f"Initializing Gadget({self.name}, {self.price})")
g = Gadget("ipad", 1500.0)
__call__ of  <class '__main__.Gadget'>
__call__ *args= ('ipad', 1500.0)
Initializing Gadget(ipad, 1500.0)

Zastosowanie metaklas

Metaklasy są nazywane “rozwiązaniem szukającym problemu”.

W zdecydowanej większości sytuacji ich użycie nie jest konieczne.

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 (np. singleton).