В Python можно описывать свои
собственные метаклассы, которые, конечно же, явно или неявно наследуются от
основного метакласса type.
Давайте
предположим, что мы бы хотели создавать классы для точек, в которых бы
автоматически появлялись атрибуты MAX_COORD и MIN_COORD.
Для этого мы создадим метакласс в виде функции (да, обычные функции тоже можно
использовать как метаклассы):
def create_class_point(name, base, attrs):
attrs.update({'MAX_COORD': 100, 'MIN_COORD': 0})
return type(name, base, attrs)
Обратите
внимание, функция-метакласс должна иметь три параметра: name – имя создаваемого класса; base – кортеж из базовых классов;
attrs – словарь с атрибутами
класса.
Сейчас
в этой функции мы в словарь attrs
просто добавляем два атрибута MAX_COORD и MIN_COORD, а
затем, явным вызовом метакласса type
формируем новый класс и возвращаем его.
Теперь,
чтобы эта функция использовалась в качестве метакласса, при объявлении класса
нужно прописать специальный параметр metaclass и передать ссылку на эту
функцию:
class Point(metaclass=create_class_point):
def get_coords(self):
return (0, 0)
Создадим
объект этого класса, обратимся к атрибуту MAX_COORD и вызовем метод get_coords:
pt = Point()
print(pt.MAX_COORD)
print(pt.get_coords())
Как
видите, все сработало, как мы и ожидали. В нашем классе автоматически появились
атрибуты MAX_COORD и MIN_COORD, а
также явно, привычным образом, прописали метод get_coords, что намного удобнее.
Фактически,
здесь класс был создан в функции create_class_point, в которую сам язык Python передал нужный набор аргументов с соответствующими значениями, а
далее, с помощью вызова объекта type был
создан этот класс. Такой подход намного удобнее, чем прописывать все атрибуты
непосредственно в словаре attrs.
Однако,
использование функции в качестве метакласса – это, скорее, учебный пример для
лучшего понимания сущности метаклассов. Все же, на практике чаще используют
отдельные классы, которые играют роль метаклассов.
Давайте
повторим нашу реализацию но не через функцию, а через класс. Вначале объявим
класс с именем Meta (имя выбираем сами, это
просто пример), который должен наследоваться от метакласса type:
class Meta(type):
def __init__(cls, name, base, attrs):
super().__init__(name, base, attrs)
cls.MAX_COORD = 100
cls.MIN_COORD = 0
Внутри
этого класса мы прописали инициализатор с четырьмя параметрами. Первый cls – это ссылка на новый уже
созданный класс, а три остальных вам уже знакомы. Внутри инициализатора мы
вызываем инициализатор базового класса, а затем, динамически добавляем два
атрибута MAX_COORD и MIN_COORD.
А
далее все то же самое. Через параметр metaclass
указываем метакласс для создания класса Point и
тестируем его работу:
class Point(metaclass=Meta):
def get_coords(self):
return (0, 0)
pt = Point()
print(pt.MAX_COORD)
print(pt.get_coords())
Однако,
инициализатор __init__() в классе Meta вызывается когда класс Point полностью создан. Для более
тонкой работы лучше переопределить магический метод __new__,
который вызывается непосредственно перед созданием класса. В нашем случае это
можно сделать так:
class Meta(type):
def __new__(cls, name_class, base, attrs):
attrs.update({'MAX_COORD': 100, 'MIN_COORD': 0})
return type.__new__(cls, name_class, base, attrs)
Здесь
записаны четыре параметра: cls – ссылка
на текущий класс Meta; name_class – имя создаваемого класса; base – кортеж из базовых классов; attrs – словарь атрибутов создаваемого класса.
Так
как метод __new__ вызывается до создания нового класса, то мы добавляем новые
атрибуты MAX_COORD и MIN_COORD
непосредственно в словарь attrs. А,
затем, вызываем аналогичный метод __new__ у
объекта-метакласса type. Обратите внимание, метод __new__ должен вернуть ссылку на созданный класс, то есть, обязательно
следует прописать оператор return.
После
запуска программы видим, что класс Point создается в соответствии с алгоритмом метакласса Meta.
Возможно,
здесь у вас возник вопрос, а в чем преимущество использования классов в
качестве метаклассов перед функциями? Принципиальных отличий здесь нет. Любой
алгоритм можно прописать и на уровне функции и на уровне класса. Однако, при
использовании классов у нас в руках оказывается вся мощь ООП. В частности,
можно создавать иерархии для формирования сложного поведения метаклассов и в
крупных проектах это заметно облегчает код и делает его гораздо читабельнее. Поэтому
на практике чаще все же используют классы из-за их удобства.
Надеюсь,
теперь вы представляете как «работают» метаклассы и как можно динамически
создавать с их помощью новые классы в программе на Python.
Наверное, здесь остался у вас главный вопрос, где все это можно применить. Но
об этом я расскажу уже на следующем занятии и покажу все на конкретном примере.