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).