Пользовательские метаклассы. Параметр metaclass

Курс по Python ООП: https://stepik.org/a/116336

В 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. Наверное, здесь остался у вас главный вопрос, где все это можно применить. Но об этом я расскажу уже на следующем занятии и покажу все на конкретном примере.

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме