Метаклассы. Объект type

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

На этом занятии мы затронем тему метаклассов. Давайте вначале разберемся, что стоит за этим умным словом. Как я уже не раз говорил на наших занятиях, булевы значения, строки, числа, списки, словари и т.п. все это объекты в языке Python. И эти объекты образованы от соответствующих классов (типов данных): bool, str, int, float, list, dict, функции и т.д. Но эти классы также являются и объектами, потому что все в Python – это объекты, даже классы. Да, классы – это объекты, которые позволяют создавать другие объекты с конкретным содержимым. А раз классы – это объекты, то должно быть нечто, что создает и их. И это нечто в Python называется метаклассом. Причем, метакласс – это тоже объект (в Python все объекты). Но это объект особого рода, который нельзя динамически порождать каким-нибудь другим мета-метаклассом. Он является вершиной, отправной точкой для создания обычных классов и, как следствие, их объектов.

Но что является метаклассом в языке Python? Наверное, вас немного удивит, но это давно знакомый вам объект type, который мы использовали для определения типов объектов. Но это, если он вызывается с одним аргументом. Если ему передать три аргумента:

type(<имя класса>, <кортеж родительских классов>, <словарь с атрибутами и их значениями>)

то данный объект начинает работать совершенно по-другому, а именно, создает (динамически) новый класс, новый тип данных в программе.

В действительности, все стандартные типы данных, функции и обычные классы – порождены метаклассом type. В этом легко убедиться, если вызвать функцию type (я ее так называю для удобства) от встроенных типов данных:

type(int)
type(bool)

или классов:

class A: pass
type(A)

Всюду увидим

<class 'type'>

Это, как раз и говорит, что все эти объекты сформированы метаклассом type.

Наверное, на этом этапе у вас в голове крутится вопрос: зачем все это надо? Мы же и так можем объявлять классы. Для чего их создавать динамически в процессе работы программы? На этот вопрос хорошо ответил гуру Питона Тим Питерс. Он сказал:

Метаклассы – это глубокая магия, о которой 99% пользователей даже не нужно задумываться. Если вы думаете, нужно ли вам их использовать — вам не нужно (люди, которым они реально нужны, точно знают, зачем они им, и не нуждаются в объяснениях, почему).

Но, на мой взгляд, чтобы лучше понимать, как функционирует язык Python, все же лучше ознакомиться с этой темой. И, кроме того, кто знает, возможно в будущем именно вам это и пригодится.

Итак, давайте теперь попробуем создать свой класс с помощью метакласса type. Для простоты сделаем определение класса точки на плоскости:

class Point:
    MAX_COORD = 100
    MIN_COORD = 0

Через объект type это будет выглядеть так:

A = type('Point', (), {'MAX_COORD': 100, 'MIN_COORD': 0})

Здесь переменная A – ссылка на новый созданный класс с именем Point. Да, так как классы – это тоже объекты, то мы можем на них ссылаться через разные переменные. И, далее, можно создать экземпляр этого класса, используя переменную A:

pt = A()

Вот мы с вами только что динамически в программе сформировали новый класс через метакласс type и увидели, что новый класс корректно работает – создает свои экземпляры.

При необходимости, можно дополнительно прописывать базовые классы, передавая их список вторым аргументом метаклассу type. Например, пусть имеются два класса:

class B1: pass
class B2: pass

И, далее, мы можем их указать в качестве базовых при создании нового класса:

A = type('Point', (B1, B2), {'MAX_COORD': 100, 'MIN_COORD': 0})

Если теперь вывести коллекцию __mro__ класса A:

A.__mro__

то увидим всю цепочку наследования:

(<class '__main__.Point'>, <class '__main__.B1'>, <class '__main__.B2'>, <class 'object'>)

Также, при динамическом создании новых классов, в них можно определять и методы. Сделать это можно, по крайней мере, двумя способами. В первом использовать уже существующую функцию, например:

def method1(self):
    print(self.__dict__)

и указать ссылку на нее при создании атрибутов класса:

A = type('Point', (), {'method1': method1})

Создадим экземпляр класса Point и вызовем этот метод:

pt = A()
pt.method1()

Как видите, все работает. Во втором способе мы можем определить метод непосредственно в словаре через лямбда-функцию:

A = type('Point', (), {'MAX_COORD': 100, 'method1': lambda self: self.MAX_COORD})
pt = A()
pt.method1()

Но так можно определять только простейшие методы. Функции со сложным поведением придется задавать отдельно, а затем, добавлять в качестве методов в создаваемый класс.

Я, думаю, вы теперь представляете, что такое метакласс и как можно динамически создавать новые классы в программе с помощью объекта type. Однако, описывать сложный алгоритм формирования новых классов непосредственно через объект type не очень удобно. Мы видели, что даже добавление новых методов происходит несколько непривычно и как-то «коряво». Благо, выход есть. В Python мы можем конструировать свои собственные метаклассы, которые, конечно, явно или неявно образуются от объекта type. Но об этом мы поговорим уже на следующем занятии.

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

Видео по теме