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'