Что такое метаклассы в python
Метаклассы в Python
Все мы знаем, что python – это объектно-ориентированный язык программирования, что означает, что он связывает данные с функциями, то есть классами.
Метакласс в Python – это класс, который создает экземпляр класса, который на самом деле является объектом другого класса и называется метаклассом. Метакласс определяет поведение объектов класса. Давайте рассмотрим несколько примеров, чтобы четко понять концепцию.
Встроенный метакласс
type – это встроенный метакласс в Python. Начнем со следующего примера:
В приведенном выше коде мы создали класс с именем SampleClass и создали из него объект.
Это означает, что obj является объектом SampleClass. Увидеть тип самого SampleClass можно следующим образом:
Это означает, что SampleClass является объектом типа класса. Чтобы быть более конкретным, SampleClass – это экземпляр типа класса, т.е. метакласс. Каждый класс принадлежит к типу встроенного метакласса.
Как работает метакласс?
Каждый раз, когда мы создаем какой-либо класс, вызывается метакласс по умолчанию. Он вызывается с тремя данными – именем класса, набором базовых классов и атрибутами класса.
Поскольку тип – это встроенный метакласс, всякий раз, когда мы создаем класс, тип вызывается с этими тремя аргументами.
Мы говорим, что каждый класс также является объектом типа, что означает, что мы можем создать любой класс в одной строке так же, как мы создаем объект любого класса. Такой способ создания называется «создание класса на лету».
Как создать класс с помощью метакласса?
Итак, используя тип метакласса, вы можете создать свой класс в одной строке, вызвав его следующим образом:
Это создаст класс с именем Student во время выполнения кода. Вышеприведенная строка эквивалентна следующему коду:
Если наследует какой-то другой класс, скажем, например, Department, тогда мы напишем следующее:
Унаследованные классы должны быть указаны во втором аргументе при создании класса на лету:
Если класс Student содержит некоторые атрибуты и функции, то они должны быть указаны в третьем аргументе как пара значений ключа. Смотрите следующие примеры:
Эти атрибуты и функции могут быть добавлены следующим образом:
Обратите внимание, что мы определили функцию, прежде чем использовать ее. Я хочу, чтобы вы поняли еще одно: первый аргумент – это имя класса. Итак, если вы напишете следующее:
Итак, лучше сохранить одно и то же имя для класса и переменной, чтобы сохранить согласованность.
Как установить?
Вы можете явно установить метакласс своего класса. Всякий раз, когда python получает класс ключевого слова, он ищет метакласс. Если не найден, то для создания объекта класса используется тип по умолчанию. Вы можете установить метакласс с помощью атрибута __metaclass__ следующим образом:
Он будет производить вывод как:
Создание
Наконец, вы также можете создать свой собственный метакласс для определения поведения любого класса, созданного с использованием вашего класса.
Для этого класс должен наследовать тип метакласса по умолчанию, поскольку он является основным. Смотрите следующий пример:
Метаклассы в Python
Метаклассы – это такие классы, экземпляры которых сами являются классами. Подобно тому, как «обычный» класс определяет поведение экземпляров класса, метакласс определяет и поведение классов, и поведение их экземпляров.
Метаклассы поддерживаются не всеми объектно-ориентированными языками программирования. Те языки программирования, которые их поддерживают, значительно отличаются по способу их реализации. Но в Python метаклассы есть.
Некоторые программисты рассматривают метаклассы в Python как «решения, которые ждут или ищут задачу».
У метаклассов множество применений. Выделим несколько из них:
Логирование и профилирование;
Регистрация классов во время создания;
Автоматическое создание свойств;
Автоматическая блокировка/синхронизация ресурсов.
Определение метаклассов
Давайте создадим совсем простой метакласс. Он ничего не умеет, кроме вывода содержимого своих аргументов в методе _new_ и возврата результата вызова type._new_ :
А теперь используем метакласс LittleMeta в следующем примере:
В главе «Type and Class Relationship» мы выяснили, что после обработки определения класса Python вызывает:
Но не в том случае, когда метакласс был объявлен в заголовке. Именно так мы и сделали в нашем прошлом примере. Наши классы Philosopher1, Philosopher2 и Philosopher3 были «прицеплены» к метаклассу EssentialAnswers. И вот почему EssentialAnswers будет вызван вместо type:
Если быть точным, то аргументам вызовов будет даны следующие значения:
Другие классы Philosopher будут вести себя аналогично.
Создаем синглтоны с помощью метаклассов
Также мы можем создавать Singleton-классы, наследуясь от Singleton, который можно определить следующим образом:
Использование метаклассов в Python
Некоторые средства метапрограммирования не так часто используются в ежедневной
работе, как обычные в ООП классы или те же декораторы. Для понимания же целей
введения подобных средств в язык требуются конкретные примеры промышленного
применения, некоторые из которых и приведены ниже.
Введение в метаклассы
Питон расширяет классическую парадигму, и сами классы в нем тоже становятся
равноправными объектами, которые можно менять, присваивать переменной и
передавать в функции. Но если класс — объект, то какому классу он соответствует?
По умолчанию этот класс (метакласс) называется type.
Простой пример
Предположим, нас утомило задание атрибутов в контрукторе __init__(self, *args,
**kwargs). Хотелось бы ускорить этот процесс таким образом, чтобы была
возможность задавать атрибуты прямо при создании объекта класса. С обычным
классом такое не пройдет:
Объект конструируется вызовом класса оператором «()». Создадим наследованием от
type метакласс, переопределяющий этот оператор:
Теперь создадим класс, использующий новый метакласс:
Расширение языка (абстрактные классы)
Ядро Python сравнительно небольшое и простое, набор встроенного инструментария
мал, что позволяет разработчикам быстро осваивать язык.
Вместе с тем, программистам, занимающимся созданием, например, фреймворков и
сопутствующих специальных подъязыков (Domain Specific Languages), предоставляются
достаточно гибкие инструменты.
Абстрактные классы (или их несколько иная форма — интерфейсы) — распространенный
и популярный среди программистов метод определения интерфейсной части
класса. Обычно такие понятия закладываются в ядро языка (как в Java или C++),
Питон же позволяет изящно и легко реализовать их собственными средствами, в
частности — при помощи метаклассов и декораторов.
Рассмотрим работу библиотеки abc из предложения по реализации для стандартной библиотеки.
Использовать асбтрактные классы очень легко. Создадим абстрактный базовый класс
с виртуальным методом и попробуем создать класс-наследник без определения этого метода:
Не вышло. Теперь определим нужный метод:
Узнаем, как это реализуется в метаклассе (опустив некоторые другие возможности
модуля abc) ABCMeta:
Метод _fix_bases добавляет скрытый класс _Abstract в число предков
абстрактного класса. Сам _Abstract проверяет, осталось ли что-нибудь во
множестве(set) __abstractmethods__; если осталось — выкидывает исключение.
В каждом абстрактном классе хранится по «замороженному» множеству(frozenset)
абстрактных методов; то есть тех методов (функций-объектов), у которых есть
атрибут __isabstractmethod__, выставляемый соответствующим декоратором:
Итак, абстрактный метод получает атрибут __isabstractmethod__ при назначении ему
декоратора. Атрибуты после наследования от абстрактного класса собираются во
множестве «__abstractmethods__» класса-наследника. Если множество не пустое, и
программист пытается создать объект класса, то будет вызвано исключение
TypeError со списком неопределенных методов.
Вывод
Просто? Просто. Язык расширен? Расширен. Комментарии, как говорится, излишни.
DSL в Django
Один из продвинутых примеров DSL — механизм ORM Django на примере класса Model и
метакласса ModelBase. Конкретно связь с базой данный здесь не интересны, имеет
смысл сконцентрироваться на создании экземпляра класса-наследника класса Model.
Большая часть следующего подраздела — подробный разбор кода
ModelBase. Читателям, не нуждающимся в подробностях, достаточно прочитать вывод
в конце раздела «Django».
Разбор метакласса ModelBase
Вся механика работы метакласса ModelBase сконцентрирована в месте
переопределения метода __new__, вызываемого непосредственно перед созданием
экземпляра класса модели:
В самом начале метода просто создается экземпляр класса и, если этот класс не
наследует от Model, просто возращается.
Все конкретные опции класса модели собираются в атрибуте класса _meta, который
может быть создан с нуля, унаследоваться от предка или быть подкорректирован в
локальном классе Meta:
Кроме того, видим, что класс может быть абстрактным, не соответствующим
какой-либо таблице в базе данных.
Момент истины в процессе создания класса модели наступает при внесении в него
параметров по умолчанию:
add_to_class либо вызывает метод contribute_to_class аргумента, либо, если
такового нет, просто добавляет именованный атрибут классу.
Класс же Options в своем contribute_to_class делает атрибут _meta ссылкой на
самого себя и собирает в нем различные параметры, вроде названия таблицы базы
данных, списка полей модели, списка виртуальных полей модели, прав доступа и
других. Он также проводит проверки связей с другими моделями на уникальность
названий полей в БД.
Далее в методе __new__ неабстрактному классу добавляются именованные
исключения:
Если класс-родитель — не абстрактный, и параметры не установлены явно в локальном
классе Meta, то наследуем параметры ordering и get_latest_by:
Менеджер по умолчанию должен быть нулевым. Если такая модель уже существует — завершаем обработку, возвращая эту модель:
Ничего особенного, просто добавляются в класс модели атрибуты, с которыми он был
создан:
Теперь требуется пройтись по полям модели и найти связи типа «один к одному»,
которые будут использовать чуть ниже:
Проход по предкам модели для наследования различных полей, с отбрасыванием тех,
что не являются наследниками Model. Далее переведены комментарии, которых
достаточно для понимания происходящего:
Абстрактные классы моделей нигде не регистрируются:
Нормальные же регистрируются и возращаются уже из списка зарегистрированных
классов моделей:
Вывод
Итак, подведем итоги. Зачем понадобились метаклассы?
1) Класс-модель должен иметь набор обязательных параметров (имя таблицы, имя
джанго-приложения, список полей, связи с другими моделями и многие другие) в
атрибуте _meta, которые и определяются при создании каждого класса, наследующего
от Model.
2) Эти параметры сложным образом наследуются от обычных и абстрактных
классов-предков, что некрасиво закладывать в сам класс.
3) Появляется возможность спрятать происходящее от программиста, использующего
фреймворк.
Замечаньица
1) Если явно не указывать наследование класса от object, то класс использует
метакласс, указанный в глобальной переменной __metaclass__, что иногда может
быть удобно при многократном использовании собственного метакласса в пределах
одного модуля. Простой пример, приведенный в начале заметки, можно переделать
следующим образом:
2) Есть такой супергуру питоновский, Тим Питерс. Он очень удачно сказал про
применение метаклассов и аналогичных средств из разряда черной магии Питона:
На русском это примерно так звучит:
Мораль тут простая: не мудрите. Метаклассы в большинстве случаев — лишнее. Питонист должен руководствоваться принципом наименьшего удивления;
менять классическую схему работы ООП не стоит просто ради самолюбования.
Ссылочки по мотивам
Английская Википедия — отсюда позаимствован простой примерчик
PEP-3119 — здесь
описываются абстрактные классы в полном своем варианте.
Ролик
на английском, подробный разговор про метаклассы в Питоне с примерами
использования. Там по ссылкам можно найти и саму статью с примерами, очень
поучительно.
Метаклассы в Python
Привет, Хабр! У нас продолжается распродажа в честь черной пятницы. Там вы найдете много занимательных книг.
Возможен вопрос: а что такое метакласс? Если коротко, метакласс относится к классу точно как класс к объекту.
Метаклассы – не самый популярный аспект языка Python; не сказать, что о них воспоминают в каждой беседе. Тем не менее, они используется в весьма многих статусных проектах: в частности, Django ORM[2], стандартная библиотека абстрактных базовых классов (ABC)[3] и реализации Protocol Buffers [4].
Это сложная фича, позволяющая программисту приспособить под задачу некоторые самые базовые механизмы языка. Именно по причине такой гибкости открываются и возможности для злоупотреблений – но нас это уже не удивляет. С большими возможностями приходит большая ответственность.
Данная тема обычно не затрагивается в различных руководствах и вводных материалах по языку, поскольку считается «продвинутой» — но и с ней надо с чего-то начинать. Я немного поискал в онлайне и в качестве наилучшего введения в тему нашел соответствующий вопрос на StackOverflow и ответы на него [1].
Поехали. Все примеры кода приведены на Python 3.6 – на момент написания статьи это новейшая версия.
Первый контакт
Мы уже кое-что успели обсудить, но пока еще не видели, что представляет собой метакласс. Скоро разберемся с этим, но пока следите за моим рассказом. Начнем с чего-нибудь простого: создадим объект.
Мы также можем объявить и наш собственный класс:
Как видим, b и B во многих отношениях действуют похоже. Можно даже сделать выражение с вызовом функции, в котором использовались бы обе переменные, просто возвращены в данном случае будут разные вещи: b возвращает 5, как и указано в определении класса, тогда как B создает новый экземпляр класса.
Это сходство – не случайность, а намеренно спроектированная черта языка. В Python классы являются сущностями первой категории[5] (ведут себя как все нормальные объекты).
Более того, если классы – как объекты, то у них обязательно должен быть собственный тип:
Оказывается, никакого двойного дна здесь нет, поскольку type относится к собственному типу.
Рассмотрим это на практике:
Это наш первый метакласс. Мы могли бы сделать его определение еще более минималистичным, но хотели сделать, чтобы в итоге он делал хотя бы что-нибудь полезное.
Все-таки, метакласс сам по себе не так интересен. Интересное начинается, лишь когда мы создаем экземпляр метакласса. Давайте это и сделаем:
Сейчас можем посмотреть все типы наших переменных:
Выше я попытался сделать мягкое введение в тему метаклассов и, надеюсь, вы уже представляете, что это такое, и как ими можно пользоваться. Но, на мой взгляд, этот текст ничего бы не стоил без нескольких практических примеров. К ним и перейдем.
Полезный пример: синглтон
В этом разделе мы напишем совсем маленькую библиотеку, в которой будет малость метаклассов. Мы реализуем «эскиз» для паттерна проектирования синглтон [6] – это класс, который может иметь всего один экземпляр.
Честно говоря, его можно было бы реализовать и без всякого использования метаклассов, просто переопределив метод __new__ в базовом классе, так, чтобы он вернул ранее запомненный экземпляр:
Рассмотрим, каков он в действии:
Эту проблему можно решить, и не прибегая никоим образом к метаклассам, но решение с ними просто очевидное – так почему бы ими не воспользоваться?
Вот что получается:
Можем попробовать, а работает ли этот подход:
Поздравляем, по-видимому наша библиотека-синглтон работает именно так, как и планировалось!
На правах опытных проектировщиков библиотеки с метаклассами, давайте замахнемся на что-нибудь посложнее.
Полезный пример: упрощенное ORM
Как упоминалось выше, с паттерном синглтон можно красиво разобраться, слегка воспользовавшись метаклассами, но острой необходимости в них нет. Большинство реальных проектов, в которых метаклассы действительно используются – это те или иные вариации на тему ORM[7].
В качестве упражнения построим подобный пример, но сильно упрощенный. Это будет уровень сериализации/десериализации между классами Python и JSON.
Вот как должен выглядеть интерфейс, который мы хотим получить (смоделирован на Django ORM/SQLAlchemy):
Мы хотим иметь возможность определять классы и их поля вместе с типами. Для этого нам пригодилась бы возможность сериализовать наш класс в JSON:
И десериализовать его:
Напоминание о конструкторе типов
Вспомните эпизод из предыдущего раздела, когда мы определяли метод __init__ для нашего первого метакласса:
name – просто имя класса в формате строки
bases – кортеж базовых классов, может быть пустым
namespace – словарь всех полей, определенных внутри класса. Сюда идут все методы и переменные класса.
Вот и все, что здесь есть. На самом деле, можно было бы и не определять класс при помощи общего синтаксиса, а вызвать конструктор type напрямую:
Упрощенное ORM – грамотная программа
Мы уже знаем, чего хотим – написать библиотеку, удовлетворяющую требованиям указанного интерфейса. Мы также знаем, что будем решать эту задачу при помощи метаклассов.
Далее я приведу реализацию в стиле грамотного программирования. Код из этого раздела можно загрузить в интерпретатор Python и запустить.
Мы будем использовать всего один пакет – для синтаксического разбора/сериализации JSON:
Далее определим базовый класс для всех наших полей. Он устроен весьма просто, как и большинство других отдельных частей данной библиотеки. В нем есть реализация-заглушка для валидационной функции и пустое начальное значение.
Если не считать перенаправления initial_value конструктору базового класса, этот код состоит в основном из процедур валидации. Опять же, не сложно добавить в него другие подобные акты валидации, но я хотел показать вам простейшую возможную модель в качестве доказательства концепции.
Следующий фрагмент кода – это наш метакласс:
Единственная вещь, для которой нам нужен наш метакласс – чтобы он подключался в момент, когда создается наш класс, брал все определения полей и сохранял их все в одном месте.
Собственно, большая часть фактической работы выполняется в базовом классе нашей библиотеки:
У класса ORMBase три метода, и у каждого из них своя конкретная задача:
to_json – простейший из всех методов в классе. Просто принимает все значения полей и сериализует их в документ JSON.
Вот и вся реализация – наша библиотека готова. Можете сами убедиться, что она работает как положено, и менять ее, если считаете, что она должна работать иначе.
Заключительные замечания
Весь код к этому посту можно скачать в репозитории на GitHub [8].
Надеюсь, эта статья вам понравилась и подсказала вам какие-то идеи. Метаклассы могут казаться немного непонятными и не всегда полезными. Однако, они определенно позволяют собирать элегантные библиотеки и интерфейсы, если уметь метаклассами пользоваться.
Подробнее о том, как метаклассы используются в реальной жизни, можно почитать в статье [9].
Продвинутый Python, часть 3: классы и метаклассы
Это завершающая статья цикла «Продвинутый Python», в которой пойдёт речь о классах и метаклассах. В первой части мы познакомились с итераторами, генераторами и модулем itertools, а во второй говорили о замыканиях, декораторах и модуле functools.
Классы как объекты
Посмотрите пример кода, чтобы ближе познакомиться с описанными особенностями.
В некоторых языках, таких как C++, классы можно объявлять только на верхнем уровне модулей. В Python class можно использовать внутри функции. Этот подход можно использовать, чтобы создавать классы на лету. Ниже пример:
Метаклассы
Обратите внимание, второй параметр должен быть кортежем, поэтому синтаксис может выглядеть странно. Если вам нужны методы, создавайте функции и передавайте их как атрибуты. Вот пример:
Можно создавать свои метаклассы: сгодится любой вызываемый (callable) объект, который способен принять три параметра и вернуть объект класса. Такие метаклассы можно применять к классу. Метакласс можно указать при объявлении класса. Давайте рассмотрим этот приём на примере, который заодно продемонстрирует возможности метаклассов:
Как видите, у декораторов и метаклассов есть много общего. Фактически, метаклассы умеют всё, что можно сделать с помощью декоратора класса. Синтаксис декораторов более простой и читабельный, поэтому по возможности следует использовать именно их. Метаклассы умеют больше, так как они запускаются перед созданием класса, а не после, как декораторы. Чтобы убедиться в этом, давайте создадим декоратор и метакласс и посмотрим на порядок исполнения.
Изучайте Python на Хекслете Первые курсы в профессии «Python-программист» доступны бесплатно. Регистрируйтесь и начинайте учиться!
Пример использования метаклассов
Рассмотрим более полезное приложение. Предположим, мы пишем набор классов для обработки ID3v2 тегов, которые используются, например, в MP3-файлах. Подробности можно узнать в «Википедии». Для реализации примера надо понимать, что теги состоят из фреймов. Каждый фрейм содержит четырёхбуквенный идентификатор. Например, TOPE — фрейм имени артиста, TOAL — фрейм названия альбома и так далее. Предположим, нам надо написать класс для каждого типа фреймов. Также нужно дать возможность пользователям библиотеки ID3v2 тегов добавлять собственные классы фреймов для поддержки новых или кастомных фреймов. С помощью метаклассов можно реализовать паттерн «фабрика классов». Это может выглядеть так:
Конечно, задачу можно решить с помощью декораторов классов. Для сравнения посмотрите, как это может выглядеть.
Как видите, можно передавать параметры в декораторы, но не в метаклассы. Если нужно передать параметры в метаклассы, это нужно делать через атрибуты. Поэтому код с декораторами чище и проще в поддержке. Заметьте, что ко времени вызова декоратора класс уже создан. Это значит, что уже поздно менять его свойства, предназначенные только для чтения.
Метаклассы, полученные из type
Подводим итоги
Посмотрим, как Python интерпретирует код выше. Затем посмотрим на вывод, чтобы подтвердить или опровергнуть наши предположения.
Метаклассы на практике
ABCMeta — это метакласс, позволяющий создавать абстрактные базовые классы. Детали смотрите в официальной документации.
Идея djungoplugins основана на статье, в которой описывается простой фреймворк плагинов для Python. Здесь метаклассы используются для создания системы расширений. Автор оригинальной публикации считает, что такой же фреймворк можно создать с помощью декораторов.
Финальный аккорд
Понимание метаклассов помогает досконально разобраться, как ведут себя объекты и классы в Python. Но применение самих метаклассов в реальности может быть сложным, как показано в предыдущем разделе. Практически всё, что можно сделать с помощью метаклассов, можно реализовать и с помощью декораторов. Поэтому прежде чем использовать метаклассы, остановитесь на минуту и подумайте, так ли они необходимы. Если можно обойтись без них, лучше пойти по этому пути. Результат будет более читабельным и простым для поддержки и отладки.
Над адаптированным переводом статьи A Study of Python’s More Advanced Features Part III: Classes and Metaclasses by Sahand Saba работали Алексей Пирогов и Дмитрий Дементий. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».