Погружение в метаклассы в Python

Пролог

Если вы не поймете некоторые вещи в этом прологе, то не расстраивайтесь. Я постараюсь объяснить последовательно, от простого к сложному. Однако для полного понимания этого текста надо знать как основы синтаксиса языка Python, так и ООП в Python.

Задумывались ли вы, почему функция isinstance (int, object) возвращает True? Объяснение, что все является объектом, которое можно часто услышать, хоть и является правдой, но не дает ответа на вопрос и на самом деле есть профанация, потому что не дает настоящего понимания, а только мнимое чувство знания. Ведь совсем непонятно, почему int (как и любой другой стандартный класс) является непременно экземпляром базового класса. Да, он является подклассом, но почему именно экземпляром? Ответ будет дан по ходу изложения.

Что такое объект?

(Кто понимает это, может смело пропустить эту главу. Она сделана для линейности повествования.)

Для тех, кто изучал Python и не пытался осознать, фраза: «В Python все есть объект!» не несет никакого смысла. Чтобы понять, что это значит, надо прежде всего знать, что такое объект. Объект — это сущность, которая имеет состояние и поведение. Состояние объекта определяется его полями, а поведение определяется его методами. Функция dir позволяет нам увидеть атрибуты объекта.

Экземпляр класса есть объект, потому что у него можно создать атрибуты (поля и методы). Классы тоже имеют атрибуты. Ниже показано, что как у классов есть атрибуты, так и у экземпляров этих классов. Вообще все, как обычно, гораздо сложнее. Вот статья, где об этом говорится подробно.

>>> class MyClass:
	n = 3
>>> MyClass.n
3
>>> instance = MyClass()
>>> instance.m = 5
>>> instance.m
5
>>> def greet():
print('hi')
>>> instance.f = greet
>>> instance.f()
hi

Приведу некоторые примеры объектов в Python:

Встроенные типы данных:

  • Целые числа: 1, -100

  • Вещественные числа: 3.14, -5.2

  • Строки: «Hello», «World»

  • Списки: [1, 2, 3], [«a», «b», «c»]

  • Кортежи: (1, 2, 3), («a», «b», «c»)

  • Множества: {1, 2, 3}, {«a», «b», «c»}

  • Словари: {«name»: «John», «age»: 30}

Объекты, созданные пользователем:

  • Функции: def my_function (): …

  • Классы: class MyClass: …

  • Экземпляры классов: my_object = MyClass ()

Стандартные библиотечные объекты:

  • Файлы: open («myfile.txt», «r»)

  • Модули: import math

Что такое метакласс?

Итак, в Python все является объектом: числа, строки, булевы значения, списки, словари и т.д. Все это объекты в языке Python. Эти объекты созданы, образованы от соответствующих классов или типов данных: int, str, bool, list, dict и т.д. Понятия класс и тип данных,  по сути, являются синонимами в Python. Но классы тоже являются объектами. То есть класс объект для создания объектов. Поэтому есть что-то, что может создавать классы. И это метакласс. Метакласс тоже объект (и в свою очередь класс). Но это особый объект, который нельзя порождать динамически (на ходу или по ходу выполнения программы). Его нельзя создать другим метаклассом. Он находится на вершине.

Здесь можно провести следующую аналогию: Есть какое-то оборудование: инструменты, машины, станки — в общем средства для производства материальных благ. Но есть то, что их производит — средства для производства средств производства — машиностроение, станкостроение или тяжелая промышленность. Конкретное значение, например, целое число 3 — материальное благо, класс — средство для производства этих материальных благ, а метаклассы –средства для производства средств производства.

Метакласс это то, что может создавать классы. Можно сказать, что метакласс создает объект-класс по созданию объектов-экземпляров класса. В Python метаклассом является объект type. Это также одноименная функция для определения типа объекта. Но функция type имеет другое применение: она также может создавать классы динамически. То есть одна и та же функция может использоваться для двух совершенно разных вещей в зависимости от передаваемых аргументов. Семантика у type будет совершенно разной в зависимости от того, сколько аргументов вы ему сообщите 1 или 3. Это сделано по историческим причинам, для сохранения обратной совместимости. type принимает на вход описание класса (имя класса, кортеж родительских классов и словарь с атрибутами) и возвращает динамически новый класс, новый тип данных.

type (obj) — определить тип

type (name, bases, dict) — создать тип

Один аргумент:

obj : Объект, тип которого требуется определить.

Три аргумента:

name : Имя для создаваемого типа (становится атрибутом __name__);

bases : Кортеж с родительскими классами (становится атрибутом __bases__);

dict : Словарь, который будет являться пространством имён для тела класса (становится атрибутом __dict__).

На самом деле все стандартные типы данных порождены метаклассом type.

>>> type(3) # Целое число порождено классом целых чисел

>>>
>>> type(int) # Класс целых чисел, в свою очередь, порожден метаклассом type

>>>
>>> class A: pass # Пользовательские классы также создаются этим метаклассом
>>> type(A)

>>>
>>> type(type) # И даже метакласс type как бы порождает сам себя

То, что type является метаклассом для самого себя, есть уловка на уровне реализации. Он является встроенным метаклассом в ядре языка и не создается каким-либо другим классом или метаклассом.

Как создать простейший класс с помощью метакласса

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

>>> class MyClass:
pass
>>> MyClass()

Python выполняет следующее:

>>> MyClass = type('MyClass', (), {})
>>> MyClass()

Здесь type создает новый класс, который называется MyClass. Аргументы в () на самом деле представляют собой кортеж базовых классов, от которых наследуется MyClass. В данном случае, поскольку MyClass не имеет явно указанного родительского класса, он наследуется от object по умолчанию. Третий аргумент {} — это словарь, содержащий атрибуты класса MyClass (как поля, так и методы).

Рассмотрим более сложный пример.

Создание класса с помощью метакласса более сложный случай. Сравнение с классическим созданием

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

Классическое создание класса

Классическое создание класса

Динамическое создание класса, с помощью метакласса

Динамическое создание класса, с помощью метакласса

Мы создали класс ChildClass сначала обычным способом, а затем так, как Python делает это под капотом.

У класса ChildClass есть родитель ParentClass, также он имеет одно поле класса species и два метода __init__ и greet.

В функцию type первым аргументов указываем имя класса ChildClass, далее указываем кортеж базовых классов (классов, от которых наследуется наш класс) это будет ParentClass и object (но object указывается автоматически) и последний аргумент словарь с атрибутами.  Первый атрибут spicies = 'Homo sapiens' (прописывается имя атрибута в виде строки и через доветочие указывается его значение). Далее надо оговориться:

Когда мы динамически создаем класс, то в него можно добавлять и методы. Методы — это тоже атрибуты. Атрибуты, которые ссылаются на объект функцию. Это возможно сделать двумя способами. Для первого способа надо предварительно создать функцию в программе. Во втором с помощью lambda-функций.

Добавим два метода __init__ и greet. Для метода __init__ вместо ссылки на реальную функцию укажем lambda-функцию. Для второго метода отдельно зададим функцию в программе и укажем имя метода и через двоеточие передадим ссылку на функцию.

Сравниваем выводы: классы родители одинаковые, доступ к полю класса species есть, можно создать экземпляр класса и передать аргументы в конструктор, через ссылку child можно обратиться к полю species и методу greet и получить значение и вывод.

То есть с помощью метакласса type динамически в программе создается новый класс ChildClass, который по структуре в точности повторяет первое (классическое) определение. type это метакласс, который Питон внутренне использует для создания всех классов.

Некоторая справка.

В Python есть две функции isinstance и issubclass.

Функция isinstance ()

Функция issubclass ()

Функция isinstance () в Python проверяет, является ли объект экземпляром указанного класса или его подкласса (обратите внимание). Она принимает два аргумента:

* Объект, который нужно проверить.

* Класс или кортеж классов, с которыми нужно сравнить объект.

Функция isinstance () возвращает True, если объект является экземпляром указанного класса или его подкласса, и False в противном случае.

Функция issubclass () в Python проверяет, является ли класс подклассом указанного класса или эквивалентен (обратите внимание) ему. Она принимает два аргумента:

* Класс, который нужно проверить.

* Класс или кортеж классов, с которыми нужно сравнить проверяемый класс.

Функция issubclass () возвращает True, если проверяемый класс является подклассом указанного класса или эквивалентен ему, и False в противном случае.

type.mro

Порядок разрешения методов (Method Resolution Order — MRO)

Демонстрация работы __mro__

Демонстрация работы __mro__

Содержит кортеж с родительскими типами, выстроенными в порядке разрешения методов.

MRO — это список классов, который определяет порядок, в котором Python ищет атрибуты и методы в иерархии классов. MRO используется несколькими методами и операторами в Python, включая issubclass (), isinstance (), а также при поиске атрибутов и методов объекта.

Перечислю несколько утверждений, которые разберем дальше:

  1. В Python все классы, явно или неявно, наследуются от базового класса object. Это означает, что все классы в Python, включая стандартные встроенные типы, такие как int, float и str, наследуют методы и атрибуты от класса object.

  2. Все встроенные (стандартные) типы создаются с помощью метакласса type (и даже object).

  3. Так как метакласс type является объектом и также классом то он тоже должен наследоваться от object.

  4. Любое конкретное значение является экземпляром класса object.

Для краткости я не буду делать много проверок. Буду брать один стандартный тип и проверять его для других результат должен быть идентичен.

Объяснение и доказательство первого пункта:

>>> issubclass(int, object) # Возвращает True так как любой встроенный класс наследуется от object
True
>>> class A: pass # Следующая проверка возвращает True так как любой пользовательский класс автоматически будет подклассом object
>>> issubclass(A, object)
True

Вопросы для закрепления:

  • Что выведет issubclass (int, int)?

  • Что выведет issubclass (object, object)?

  • Что выведет issubclass (type, type)?

>>> issubclass(int, int)
True
>>> issubclass(object, object)
True
>>> issubclass(type, type)

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

Объяснение и доказательство второго пункта:

>>> isinstance(int, type) # Это корректный для нас результат так как в начале я показывал это
True
>>> isinstance(object, type) # И даже класс стоящий на вершине иерархии object будет экземпляром метакласса type. Возвращает True так как object есть класс, а значит порожден метаклассом.
True

Еще немного вопросов, ответ на второй вопрос будет дан после доказательства утверждения номер три:

  • Что выведет isinstance (3, int)?

  • Что выведет isinstance (int, object)?

  • Что выведет isinstance (bool, int)?

>>> isinstance(3, int) # Этот результат довольно очевиден. Целые числа создаются с помощью класса int
True
>>> isinstance(int, object) # Вывод этой проверки не понятен
True
>>> isinstance(bool, int) # bool является подклассом, а не экземпляром int
False

Вторая проверка вводит в ступор. Неужели базовый object является метаклассом, который порожадет все типы данных? Но в самом начале я показывал, что встроенные типы порождаются type. Противоречие?

Подумайте над следующими вопросами перед объяснением третьего утверждения, чтобы лучше прочувствовать их:

  • Что выведет isinstance (type, object)?

  • Что выведет isinstance (type, type)?

  • Что выведет isinstance (object, object)?

Объяснение и доказательство третьего пункта (самого интересного потому что даст ответ на вопрос из пролога):

>>> issubclass(type, object) # Возвращает True так как type также является классом и должен, как и все классы, наследоваться от object.
True
>>> isinstance(type, object) # А isinstance проверяет все подклассы, а раз у object подкласс type, то будет выполнена следующая проверка.
True
>>> isinstance(type, type) # type как бы порождает сам себя (я уже об этом говорил). Это сделано специально, чтобы не существовала бесконечно метаклассов друг над другом.
True
>>> isinstance(object, object) # Также как и во второй проверке isinstance проверяет все подклассы, а раз у object подкласс type, то будет сделана последняя проверка из пункта два. И проверка возвращает True так как object является экземпляром от type.
True

То есть object есть экземпляр типа type (предыдущий пункт). Более того, обратное утверждение тоже верно. Но только если мы делаем проверку функцией isinstance (). На самом деле это, конечно, не правда, потому что object — обычный класс и не является метаклассом, тем более порождающим метакласс type.

Теперь, наконец, ответ на вопрос из пролога, который вы теперь можете дать сами.

Но перед этим вопрос на понимание:

>>> issubclass(object, type) # Возвращает False потому что object является суперклассом для всех остальных классов.
False

Почему же isinstance (int, object) возвращает True?

Может возникнуть заблуждение, что если класс A является экземпляром класса B (то есть isinstance (A, B) равно True), то можно создать экземпляры класса A с помощью класса B. Однако это не всегда верно.

То есть неправда то, что если int является экземпляром класса object (здесь говорится именно что isinstance (int, object) есть True и это может сильно запутать потому что на самом деле это не так, но из-за того, что метакласс type является классом и наследуется от object, а isinstance проверяет подклассы, проверка возвращает True), то значит можно создать int через объект object. Подобно тому как object создается через type. Но при этом isinstance (type, object) тоже True, а на самом деле False. Хотя если дословно переводить название функции (является экземпляром/есть экземпляр) может показаться, что object есть метакласс над самым «верхним» метаклассом type. Это происходит из описанного выше запутанного наследования в Python.

Логика примерно такая: если int порождает объекты целого типа (например 3), то isinstance (3, int) возвращает True, если что-то порождает int (это метакласс type), то isinstance (int, type) возвращает True, но object не является метаклассом. Так почему isinstance (int, object) возвращает True, ведь object не создает int, а просто наследует.

Эта запутанность возникает если не знать про то, что type подкласс object (об этом не говорит переменная mro когда вызывается для класса int, потому что как классы они на одной ступени, но как экземпляр int ниже type). То есть не учитывать существование промежуточного метакласса type. Поэтому я составил следующую иерархию для лучшего понимания. Не уверен, что вторую иерархию можно так изобразить. Но чтобы усвоить, что я имею в виду, лучше взглянуть.

Когда рассматриваем классы (кто кого наследует):

object
|
+--> type, int, … (все собственные и встроенные типы данных)

Я для простоты написал все типы данных, понятно, что можно выделить больше ступеней, например, int наследует bool, но для понимания достаточно этого

Когда рассматриваем экземпляры (кто кого инстанцирует):

type
|
+--> Метаклассы (не обязательная ступень)
|
+--> int, str, … (все собственные и встроенные типы данных)
|
+ --> объекты целого типа, строки, …

При этом будет ошибкой как в наследовании сказать, что type порождает объекты целого типа (если проводить аналогию с классами, то object родительский класс для любого подкласса, даже если он имеет посредников). Поэтому эта схема довольно условная. isinstance (3, type) есть False так как у type нет потомков классов (он может наследовать только метаклассы) и isinstance просто не найдет класс int, который и создает объекты целого типа.

И, как говорится, проницательный читатель может спросить: Не возникает ли парадокса в том, как type может наследоваться от object, если он еще не создан? А чтобы type мог его создать, ему надо наследоваться от object (object создается классом type, а type, в свою очередь, наследуется от object).

Чуть подробнее и немного занудно об этом парадоксе (кто понял вопрос выше может пропустить):

object создается метаклассом type: type является встроенным метаклассом, используемым для создания новых классов в Python. Когда создается новый класс, метакласс type используется для его создания.

type наследуется от базового класса object: метакласс type также является классом и, как и все классы в Python, он наследуется от object. Это означает, что класс type унаследовал все атрибуты и методы, определенные в object.

Объяснение данного парадокса

Я попытаюсь дать объяснение, как он может разрешаться. Однако не утверждаю, что происходит именно так. Это подходит для объяснения себе (кто знает, как это происходит на самом деле, прошу в комментарии).

Объект type является встроенным в ядре языка и не создается каким-либо другим метаклассом и присутствует в интерпретаторе Python с самого начала. Это означает, что он не создается во время выполнения программы. Класс object создается с помощью функции type () во время инициализации интерпретатора Python.

Порядок событий следующий:

  1. Интерпретатор Python запускается.

  2. Интерпретатор Python создает класс object с помощью функции type ().

  3. Класс type определяется как потомок класса object.

  4. Все остальные классы в Python создаются с помощью функции type (), которая теперь является потомком класса object.

Может быть класс object также существует в ядре. И то, что type наследуется от object, а object создается type, делается на уровне реализации специально.

Ну и объяснение четвертого пункта:

>>> isinstance(3, int) # Возвращает True так как целое число 3 создается классом int.
True
>>> isinstance(3, object) # Возвращает True так как потомком object является класс int, который в свою очередь создает конкретные значения целого типа (как видно выше).
True

Думаю, тут все понятно и без объяснений.

Создание пользовательских метаклассов

Именно потому что метаклассы также, в свою очередь, являются классами, они имеют магические методы унаследованные от object.

Методы new и init у метакласса

Метод __new__ метакласса

Метод __init__ метакласса

Метод __new__ метакласса вызывается при создании нового класса. Он используется для создания нового экземпляра класса и возвращает его. Магический метод new вызывается непосредственно перед созданием класса.

Аргументы:

cls: Сам метакласс.

name: Имя нового класса.

bases: Кортеж базовых классов нового класса.

attrs: Словарь атрибутов нового класса.

Метод __init__ метакласса вызывается после создания нового класса, но до его инициализации. Он используется для инициализации класса и выполнения любой необходимой настройки для нового класса. Конструктор срабатывает сразу же после создания объекта (класса).

Аргументы:

cls: Сам класс.

name: Имя нового класса.

bases: Кортеж базовых классов нового класса.

attrs: Словарь атрибутов нового класса.

Использовать метакласс type напрямую, чтобы создавать классы не удобно. Даже добавление методов вызывает трудности. Поэтому в Python можно конструировать свои собственные метаклассы, которые явное или неявно используют метакласс type.

Приведу два способа объявить свой собственный метакласс. Вообще пользовательский метакласс может быть любым вызываемым объектом .

Первый способ

Создание метакласса с помощью функции

Создание метакласса с помощью функции

Первый способ реализуется используя функцию. Она принимает три параметра: имя класса, список базовых классов и словарь атрибутов класса.

Функция возвращает то, что вернет функция type. Этому метаклассу type мы передаем все эти три параметра. Еще у нас должен добавляться атрибут description. Вот так вот можно описать простейший метакласс с одним атрибутом. И далее его применяем. Пишем наш класс и в круглых скобках указываем специальный параметр metaclass и передаем ему ссылку на нужный метакласс.

При создании класса MyClass будет отрабатывать метакласс my_metaclass и будет создаваться класс MyClass с уже добавленным атрибутом. Для всех классов созданных с помощью данного метакласса будет автоматически проставляться определенный атрибут.

Заметьте, что метод в этом классе мы определили привычным способом, а не отдельно в программе как раньше, это преимущество использования собственных метаклассов. Все атрибуты внутри класса автоматически попадают в словарь attrs. И затем когда вызыватся метакласс type с этим словарем, то в нем уже присутствует этот метод.

Как выполняется создание класса

То есть когда отрабатывает функция my_metaclass интерпретатор языка Python автоматически передает имя (MyClass), кортеж из базовых классов и словарь атрибутов, которые мы или явно (description), или неявно (в самом классе метод my_method) передали в словарь attrs. И затем формируется новый класс MyClass.

Второй способ

Второй способ реализуется с помощью классов. Объявим класс MyMetaclass и так как это метакласс, то он должен наследоваться от метакласса type.

Создание метакласса с помощью класса

Создание метакласса с помощью класса

Здесь добавлять атрибуты и методы можно либо через конструктор, либо для более тонкой настройки лучше переопределять магический метод new. В этом примере атрибут добавляется через магический метод new и еще есть дополнительная функциональность (по сравнению с примером выше), которая получается за счет вывода информации как из new так и из init. Здесь можно использовать всю мощь ООП, чего нельзя сделать в функции. Поэтому на практике чаще всего поступают именно так.

Разница через что добавлять атрибуты, только в том, что когда переопределяется магический метод new, то атрибуты добавляются через словарь attrs (потому что класс еще не создан). А в init работаем через ссылку на класс для непосредственного создания атрибутов класса.

Как выполняется создание класса

Когда будет отрабатывать строчка с определением класса MyClass, то вызовется класс MyMetaclass, в коллекцию attrs будут переданы все атрибуты, что присутствуют внутри класса MyClass (метод my_method), в base будет передан кортеж из базовых классов, в name будет передано имя класса MyClass. В new в cls будет передана ссылка на сам метакласс. В init в cls будет передана ссылка на уже созданный класс MyClass. И три остальные параметры как и раньше. Все это Python делает за нас автоматически и в этом огромное преимущество перед просто использованием объекта type.

Зачем нужны метаклассы. Примеры

Далее я привожу области применения, но не показываю на примерах. Конкретные примеры можно найти в этой статье.

Django ORM

Singleton

ABCMeta

• Область применения: Создание и настраивание моделей базы данных.

• Как: Django использует метаклассы для автоматического создания таблиц базы данных и методов доступа к данным для моделей. Метаклассы также позволяют настраивать поведение моделей, например, добавлять ограничения или переопределять методы сохранения.

• Зачем: Метаклассы упрощают работу с базами данных в Django, предоставляя автоматизированный и настраиваемый способ создания и управления моделями.

• Область применения: Создание классов, которые имеют только один экземпляр.

• Как: Метаклассы можно использовать для перехвата создания нового экземпляра класса и возврата существующего экземпляра вместо этого.

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

• Область применения: Создание абстрактных базовых классов (ABC) и проверка их реализации подклассами.

• Как: Метакласс ABCMeta предоставляет функциональность ABC в Python. Он проверяет, реализованы ли абстрактные методы в подклассах, и поднимает исключение, если это не так.

• Зачем: ABCMeta помогает гарантировать, что подклассы реализуют необходимый интерфейс, обеспечивая согласованность и полноту в иерархии классов.

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

© Habrahabr.ru