Привет, Хабр! У нас продолжается распродажа в честь черной пятницы. Там вы найдете много занимательных книг. Возможен вопрос: а что такое метакласс? Если коротко, метакласс относится к  классу точно как класс к объекту. Метаклассы – не самый популярный аспект языка Python; не сказать, что о них воспоминают в каждой беседе. Тем не менее, они используется в весьма многих статусных проектах: в частности, Django ORM, стандартная библиотека абстрактных базовых классов (ABC) и реализации Protocol Buffers . Это сложная фича, позволяющая программисту приспособить под задачу некоторые самые базовые механизмы языка. Именно по причине такой гибкости открываются и возможности для злоупотреблений – но нас это уже не удивляет. С большими возможностями приходит большая ответственность.  Данная тема обычно не затрагивается в различных руководствах и вводных материалах по языку, поскольку считается «продвинутой» — но и с ней надо с чего-то начинать. Я немного поискал в онлайне и в качестве наилучшего введения в тему нашел соответствующий вопрос на StackOverflow и ответы на него . Поехали. Все примеры кода приведены на Python 3.6 – на момент написания статьи это новейшая версия. Мы уже кое-что успели обсудить, но пока еще не видели, что представляет собой метакласс. Скоро разберемся с этим, но пока следите за моим рассказом. Начнем с чего-нибудь простого: создадим объект. Мы создали новый object и сохранили ссылку на него в переменной o. Тип o – это object. Мы также можем объявить и наш собственный класс: Теперь у нас две плохо названные переменные a и o, и мы можем проверить, в самом ли деле они относятся к соответствующим классам: Выше заметна одна интересная вещь: объект a также относится к типу object. Ситуация такова, поскольку класс A является подклассом object (все классы, определяемые пользователем, наследуют от object). Еще одна интересная вещь – во многих контекстах мы можем взаимозаменяемо применять переменные a и A. Для таких функций как print невелика разница, какую переменную мы ей выдадим, a или A – оба вызова «что-то» выведут на экран.  Давайте поподробнее рассмотрим класс B, который мы только что определили: Как видим, b и B во многих отношениях действуют похоже. Можно даже сделать выражение с вызовом функции, в котором использовались бы обе переменные, просто возвращены в данном случае будут разные вещи: b возвращает 5, как и указано в определении класса, тогда как B создает новый экземпляр класса. Это сходство – не случайность, а намеренно спроектированная черта языка. В  Python классы являются сущностями первой категории (ведут себя как все нормальные объекты). Более того, если классы – как объекты, то у них обязательно должен быть собственный тип: Оказывается, что и object, и A относятся к классу type – type это "метакласс, задаваемый по умолчанию ". Все остальные метаклассы должны наследовать от него. Возможно, на данном этапе вас уже немного путает, что класс имеет имя type, но в то же время это и функция, возвращающая тип сообщаемого объекта (семантика у type  будет совершенно разной в зависимости от того, сколько аргументов вы ему сообщите – 1 или 3). В таком виде его сохраняют по историческим причинам. Как object, так и A также являются экземплярами object – в конечном итоге, все они объекты. Каков же в таком случае тип type, могли бы вы спросить?  Оказывается, никакого двойного дна здесь нет, поскольку type относится к собственному типу. Весь фокус, заключающийся в метаклассах: мы создали A, подкласс object, так, чтобы новый экземпляр a относился к типу A и, следовательно, object. Таким же образом можно создать подкласс от type под названием Meta. Впоследствии мы можем использовать его как тип для новых классов; они будут экземплярами обоих типов: type и Meta. Рассмотрим это на практике: Это наш первый метакласс. Мы могли бы сделать его определение еще более минималистичным, но хотели сделать, чтобы в итоге он делал хотя бы что-нибудь полезное. Он переопределяет магический метод __init__,  чтобы на экран выводилось сообщение всякий раз, когда создается новый экземпляр Meta. Он переопределяет магический метод call , чтобы выводилось сообщение ·         всякий раз, когда пользователь применяет синтаксис вызова функций к экземпляру – пишет variable(). Оказывается, что в Python создание экземпляра класса имеет ту же форму, что и вызов функции. Если у нас есть функция f, то, чтобы вызвать ее, мы пишем f() . Если у нас есть класс A, то мы пишем A() для создания нового экземпляра. Соответственно, мы используем хук __call__. Все-таки, метакласс сам по себе не так интересен. Интересное начинается, лишь когда мы создаем экземпляр метакласса. Давайте это и сделаем: Действительно, наш метакласс работает как задумано: выводит сообщения, когда в жизненном цикле класса происходят определенные события. В данном случае важно понимать, что мы работаем сразу с тремя разными уровнями абстракции - метаклассом, классом и экземпляром. Когда мы пишем class C(metaclass=Meta), мы создаем C, представляющий собой экземпляр Meta - вызывается Meta.init, и выводится сообщение. На следующем шаге мы вызываем C() для создания нового экземпляра класса C, и на этот раз выполняется Meta.__call__. На последнем шаге мы вывели на экран c, вызывая C.__str__, который, в свою очередь, разрешается в заданную по умолчанию реализацию, определенную в базовом классе object. Сейчас можем посмотреть все типы наших переменных:  Выше я попытался сделать мягкое введение в тему метаклассов и, надеюсь, вы уже представляете, что это такое, и как ими можно пользоваться. Но, на мой взгляд, этот текст ничего бы не стоил без нескольких практических примеров. К ним и перейдем. В этом разделе мы напишем совсем маленькую библиотеку, в которой будет малость метаклассов. Мы реализуем "эскиз" для паттерна проектирования синглтон   – это класс, который может иметь всего один экземпляр.  Честно говоря, его можно было бы реализовать и без всякого использования метаклассов, просто переопределив метод __new__ в базовом классе, так, чтобы он вернул ранее запомненный экземпляр: Вот и все. Любой подкласс, наследующий от SingletonBase, теперь проявляет поведение синглтона. Рассмотрим, каков он в действии: Тот подход, который мы здесь используем, вроде бы работает – при каждой попытке создать экземпляр возвращается тот же самый объект. Но есть и такое поведение, которое может показаться нам неожиданным: при попытке создать экземпляр класса B мы получаем в ответ тот же самый экземпляр A, что и раньше.  Эту проблему можно решить, и не прибегая никоим образом к метаклассам, но решение с ними просто очевидное – так почему бы ими не воспользоваться?  У нас будет такой класс SingletonBaseMeta, чтобы каждый его подкласс при создании инициализировал поле instance со значением None. Вот что получается: Можем попробовать, а работает ли этот подход: Поздравляем, по-видимому наша библиотека-синглтон работает именно так, как и планировалось! На правах опытных проектировщиков библиотеки с метаклассами, давайте замахнемся на что-нибудь посложнее. Как упоминалось выше, с паттерном синглтон можно красиво разобраться, слегка воспользовавшись метаклассами, но острой необходимости в них нет. Большинство реальных проектов, в которых метаклассы действительно используются – это те или иные вариации на тему ORM. В качестве упражнения построим подобный пример, но сильно упрощенный. Это будет уровень сериализации/десериализации между классами Python и JSON. Вот как должен выглядеть интерфейс, который мы хотим получить (смоделирован на Django ORM/SQLAlchemy): Мы хотим иметь возможность определять классы и их поля вместе с типами. Для этого нам пригодилась бы возможность сериализовать наш класс в JSON: И десериализовать его: Для всего вышеприведенного нам не так уж и нужны метаклассы, так что давайте реализуем одну «изюминку» - добавим  валидацию. Прежде чем перейти к реализации ORM-библиотеки, я должен напомнить еще об одной вещи, конструкторе типов type. Я упоминал его лишь вскользь, но это важная тема, которую требуется развернуть. Вспомните эпизод из предыдущего раздела, когда мы определяли метод __init__  для нашего первого метакласса: Откуда же взялись эти три аргумента name, bases и namespace? Это параметры конструктора типов. Три этих значения полностью описывают класс, создаваемый в данный момент. name – просто имя класса в формате строки bases – кортеж базовых классов, может быть пустым namespace – словарь всех полей, определенных внутри класса. Сюда идут все методы и переменные класса. Вот и все, что здесь есть. На самом деле, можно было бы и не определять класс при помощи общего синтаксиса, а вызвать конструктор type напрямую: В этом коде мы определили два почти идентичных класса, A и B.У них отличаются значения, присвоенные переменной класса X, и выводятся на экран разные значения при вызове метода f. Но на этом все – фундаментальных отличий нет, и оба принципа определения классов эквивалентны. Фактически, интерпретатор Python преобразует первый из описанных здесь механизмов во второй. Именно на этом этапе определение собственного метакласса позволяет вам влиять на события. Можно перехватывать параметры, передаваемые конструктору type, изменять их и создавать собственный класс таким образом, как вам угодно.  Мы уже знаем, чего хотим – написать библиотеку, удовлетворяющую требованиям указанного интерфейса. Мы также знаем, что будем решать эту задачу при помощи метаклассов. Далее я приведу реализацию в стиле грамотного программирования. Код из этого раздела можно загрузить в интерпретатор Python и запустить. Мы будем использовать всего один пакет – для синтаксического разбора/сериализации JSON: Далее определим базовый класс для всех наших полей. Он устроен весьма просто, как и большинство других отдельных частей данной библиотеки. В нем есть реализация-заглушка для валидационной функции и пустое начальное значение. Для простоты я реализую всего два подкласса Field: IntField и StringField. При необходимости можно добавить и другие.  Если не считать перенаправления initial_value конструктору базового класса, этот код состоит в основном из процедур валидации. Опять же, не сложно добавить в него другие подобные акты валидации, но я хотел показать вам простейшую возможную модель в качестве доказательства концепции. В StringField мы хотим проверить, относится ли значение к правильному типу – str, и является ли длина строки меньшей или равной максимальному значению (если такое значение определено). В поле IntField мы проверяем, является ли значение целым числом, и является ли оно меньшим или равным, чем сообщенное максимальное значение. Важно отметить: мы допускаем, чтобы значения в полях были равны None. В качестве интересного упражнения предлагаю читателю реализовать обязательные поля, в которых не допускается значение None. Следующий фрагмент кода – это наш метакласс: Наш метакласс совсем не кажется сложным. В нем одна функция, и единственное его назначение – собрать все экземпляры Field в новую переменную класса, которая называется _fields. Все экземпляры полей также удаляются из словаря класса. Единственная вещь, для которой нам нужен наш метакласс – чтобы он подключался в момент, когда создается наш класс, брал все определения полей и сохранял их все в одном месте. Собственно, большая часть фактической работы выполняется в базовом классе нашей библиотеки: У класса ORMBase три метода, и у каждого из них своя конкретная задача: __init__ - первым делом, установить все поля в начальные значения. Затем, если в качестве параметра передается документ в формате JSON, разобрать его и присвоить значения, полученные в процессе считывания, полям нашей модели.   __setattr__ - Это магический метод, вызываемый всякий раз, когда кто-нибудь пытается присвоить значение атрибуту класса. Когда кто-нибудь записывает object.attribute = value, вызывается метод object.__setattr__("attribute", value). Переопределив этот метод, мы можем изменить поведение, заданное по умолчанию, в данном случае – при помощи инъекции валидационного кода. to_json – простейший из всех методов в классе. Просто принимает все значения полей и сериализует их в документ JSON. Вот и вся реализация – наша библиотека готова. Можете сами убедиться, что она работает как положено, и менять ее, если считаете, что она должна работать иначе. Весь код к этому посту можно скачать в репозитории на GitHub . Надеюсь, эта статья вам понравилась и подсказала вам какие-то идеи. Метаклассы могут казаться немного непонятными и не всегда полезными. Однако, они определенно позволяют собирать элегантные библиотеки и интерфейсы, если уметь метаклассами пользоваться. Подробнее о том, как метаклассы используются в реальной жизни, можно почитать в статье . [1] http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python [2] https://docs.djangoproject.com/en/1.10/topics/db/ [3] https://docs.python.org/3/library/abc.html#abc.ABCMeta [4] https://developers.google.com/protocol-buffers/docs/pythontutorial [5] https://ru.wikipedia.org/wiki/Объект_первого_класса [6] https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования) [7] https://ru.wikipedia.org/wiki/ORM [8] https://github.com/MillionIntegrals/metaclass-playground [9] http://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example