Продвинутый Python, часть 3: классы и метаклассы

Это завершающая статья цикла «Продвинутый Python», в которой пойдёт речь о классах и метаклассах. В первой части мы познакомились с итераторами, генераторами и модулем itertools, а во второй говорили о замыканиях, декораторах и модуле functools.
Содержание
- Классы как объекты
- Метаклассы
- Пример использования метаклассов
- Метаклассы, полученные из
type
- Подводим итоги
- Метаклассы на практике
- Финальный аккорд
Классы как объекты
Классы в Python — это объекты, как и функции. Сразу после объявления класса Python создаёт объект класса и присваивает его переменной с именем класса. Классы — это объекты с типом type
. Из этого правила есть исключения, о которых пойдёт речь ниже.
Объекты класса можно вызывать, то есть в них есть метод __call__
. При вызове создаётся объект соответствующего класса. С классами можно обращаться как с другими объектами. Например, можно определять атрибуты, присваивать классы переменным, использовать их там, где требуется вызываемая сущность, например, в map
. Когда вы пишете map(str, [1, 2, 3])
, список чисел конвертируется в список строк, так как str
— это класс.
Посмотрите пример кода, чтобы ближе познакомиться с описанными особенностями.
В некоторых языках, таких как C++, классы можно объявлять только на верхнем уровне модулей. В Python class
можно использовать внутри функции. Этот подход можно использовать, чтобы создавать классы на лету. Ниже пример:
В этом примере классы, созданные с помощью make_class
, это разные объекты. Поэтому объекты, созданные этими классами, имеют разный тип. В данном случае устанавливаем имя класса вручную после создания класса. Это похоже на работу с декораторами. Также заметьте, что метод print_class_name
созданного класса захватывает его замыкание, в котором есть class_name
. Если вы не очень уверенно работаете с замыканиями, самое время перечитать вторую статью из цикла «Продвинутый Python», в которой рассматривались замыкания и декораторы.
Метаклассы
Если классы — это объекты, которые создают объекты, то как называются объекты, которые создают классы? Поверьте, это не загадка «яйцо или курица». Здесь есть чёткий ответ: такие объекты называются метаклассами. Самым простым метаклассом можно считать type
. Когда type
получает на вход один параметр, он возвращает тип объекта, переданного в качестве параметра. В данном случае он не работает как метакласс. Когда type
получает на вход три параметра, он работает как метакласс и создаёт класс на основе переданных параметров. В качестве параметров должны передаваться имя класса, родители (классы, от которых происходит наследование), словарь атрибутов. Последние два параметра могут быть пустыми. Вот пример кода:
Обратите внимание, второй параметр должен быть кортежем, поэтому синтаксис может выглядеть странно. Если вам нужны методы, создавайте функции и передавайте их как атрибуты. Вот пример:
Можно создавать свои метаклассы: сгодится любой вызываемый (callable) объект, который способен принять три параметра и вернуть объект класса. Такие метаклассы можно применять к классу. Метакласс можно указать при объявлении класса. Давайте рассмотрим этот приём на примере, который заодно продемонстрирует возможности метаклассов:
В примере выше C
оказывается переменной, которая указывает на строку 'Hello'
. Конечно, вряд ли кто-то в здравом уме будет писать такой код. Мы просто хотим посмотреть, как работают метаклассы. Теперь давайте создадим что-то более практичное. В предыдущей статье серии мы видели, как с помощью декоратора можно логировать каждый метод в классе. Давайте сделаем то же самое с помощью метакласса. Позаимствуем декоратор logged
из поста о декораторах и итераторах.
# Output:
Creating class C
- Running 'C.__init__' on Nov 21 2019 - 12:56:59
- Finished 'C.__init__', execution time = 0.000s
- Running 'C.print_x' on Nov 21 2019 - 12:57:06
Test
- Finished 'C.print_x', execution time = 0.000s
Как видите, у декораторов и метаклассов есть много общего. Фактически, метаклассы умеют всё, что можно сделать с помощью декоратора класса. Синтаксис декораторов более простой и читабельный, поэтому по возможности следует использовать именно их. Метаклассы умеют больше, так как они запускаются перед созданием класса, а не после, как декораторы. Чтобы убедиться в этом, давайте создадим декоратор и метакласс и посмотрим на порядок исполнения.
# Output:
In metaclass, creating the class.
In decorator, chance to modify the class.
Creating object.
Изучайте Python на Хекслете
Первые курсы в профессии «Python-программист» доступны бесплатно. Регистрируйтесь и начинайте учиться!
Пример использования метаклассов
Рассмотрим более полезное приложение. Предположим, мы пишем набор классов для обработки ID3v2 тегов, которые используются, например, в MP3-файлах. Подробности можно узнать в «Википедии». Для реализации примера надо понимать, что теги состоят из фреймов. Каждый фрейм содержит четырёхбуквенный идентификатор. Например, TOPE
— фрейм имени артиста, TOAL
— фрейм названия альбома и так далее. Предположим, нам надо написать класс для каждого типа фреймов. Также нужно дать возможность пользователям библиотеки ID3v2 тегов добавлять собственные классы фреймов для поддержки новых или кастомных фреймов. С помощью метаклассов можно реализовать паттерн «фабрика классов». Это может выглядеть так:
# Output:
Creating class ID3v2Frame
Creating class ID3v2TitleFrame
Creating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>
Конечно, задачу можно решить с помощью декораторов классов. Для сравнения посмотрите, как это может выглядеть.
Decorating class ID3v2Frame
Decorating class ID3v2TitleFrame
Decorating class ID3v2CommentFrame
<class '__main__.ID3v2TitleFrame'>
<class '__main__.ID3v2CommentFrame'>
Как видите, можно передавать параметры в декораторы, но не в метаклассы. Если нужно передать параметры в метаклассы, это нужно делать через атрибуты. Поэтому код с декораторами чище и проще в поддержке. Заметьте, что ко времени вызова декоратора класс уже создан. Это значит, что уже поздно менять его свойства, предназначенные только для чтения.
Метаклассы, полученные из type
Как сказано выше, самый простой метакласс — это type
, и полученные из него классы имеют тип type
. Здесь возникает естественный вопрос: что представляет собой тип type
. Ответ простой: type
. Это значит, что type
представляет собой класс, и он выступает в качестве своего метакласса. Это экстраординарно, и это стало возможным на уровне интерпретатора Python. Вручную написать класс, который выступает в качестве своего метакласса, невозможно.
Понимая, что type
представляет собой класс, мы можем написать классы, которые наследуются от него. Эти классы можно использовать как метаклассы. Классы, которые их используют, будут иметь такой же тип, как метаклассы, полученные из type
. Вот пример:
При вызове класса для создания нового объекта вызывается его функция __call__
. Она вызывает type.__call__
для создания объекта. В следующем разделе подытожим рассмотренное выше.
Подводим итоги
Предположим, что некий класс C
имеет метакласс my_metaclass
и декорирован с помощью my_class_decorator
. Далее предположим, что my_metaclass
представляет собой класс, полученный из type
. Соберём всё вместе, чтобы увидеть, как создаётся C
и как создаются объекты его типа. Вот как выглядит код:
На этом этапе вы можете потратить пару минут и попробовать определить порядок исполнения print
.
Посмотрим, как Python интерпретирует код выше. Затем посмотрим на вывод, чтобы подтвердить или опровергнуть наши предположения.
- Python читает определение класса и готовится передать три параметра в метакласс. Вот параметры:
class_name
,parents
иattributes
. - В нашем случае метакласс представляет собой класс, поэтому его вызов похож на создание нового класса. Это значит, что первый
my_metaclass.__new__
вызывается с четырьмя параметрами. Так создаётся объект, который и станет классом с именемC
. У объекта вызывается__init__
, а затем в переменную C записывается ссылка на объект. - Затем Python смотрит на декораторы, которые можно применить к классу. В нашем случае есть только один декоратор. Python вызывает его, передаёт возвращённый из метакласса класс в качестве параметра. Класс заменяется объектом, который возвращается из декоратора.
- Тип класса будет таким же, как определено в метаклассе.
- Когда класс вызывается для создания нового объекта, Python ищет
__call__
в метаклассе, так как тип класса — метакласс. В нашем случаеmy.metaclass.__call__
просто вызываетtype.__call__
, который создаёт объект из переданного класса. - Затем
type.__call__
создаёт объект. Для этого он ищетC.__new__
и запускает его. - Возвращённый объект готов к использованию.
Основываясь на этой логике, можно ожидать, что my_metaclass.__new__
вызывается первым. Затем следует my_metaclass.__init__
, затем my_class_decorator
. В этот момент класс C
полностью готов к использованию. Когда мы вызываем C
для создания объекта, который вызывает my.metaclass.__call__
(каждый раз при вызове объекта Python пытается вызвать __call__
), затем type.__call__
вызывает C.__new__
, наконец, вызывается C.__init__
. Вот вывод:
- my_metaclass.__new__ - Creating class instance of type <class '__main__.my_metaclass'>
- my_metaclass.__init__ - Initializing the class instance <class '__main__.C'>
- my_class_decorator - Chance to modify the class <class '__main__.C'>
- my_metaclass.__call__ - Creating object of type <class '__main__.C'>
- C.__new__ - Creating object.
- C.__init__ - Initializing object.
Object c = <__main__.C object at 0x7fef85a8ecf8>
Метаклассы на практике
Метаклассы — мощный инструмент, хотя и скорее эзотерический. Но достойных применений метаклассов известно не так уж и много. Автор оригинальной публикации нашёл только два репозитория, в которых метаклассы применяются по-настоящему. Это ABCMeta
и djangoplugins
.
ABCMeta
— это метакласс, позволяющий создавать абстрактные базовые классы. Детали смотрите в официальной документации.
Идея djungoplugins
основана на статье, в которой описывается простой фреймворк плагинов для Python. Здесь метаклассы используются для создания системы расширений. Автор оригинальной публикации считает, что такой же фреймворк можно создать с помощью декораторов.
Финальный аккорд
Понимание метаклассов помогает досконально разобраться, как ведут себя объекты и классы в Python. Но применение самих метаклассов в реальности может быть сложным, как показано в предыдущем разделе. Практически всё, что можно сделать с помощью метаклассов, можно реализовать и с помощью декораторов. Поэтому прежде чем использовать метаклассы, остановитесь на минуту и подумайте, так ли они необходимы. Если можно обойтись без них, лучше пойти по этому пути. Результат будет более читабельным и простым для поддержки и отладки.
Над адаптированным переводом статьи A Study of Python's More Advanced Features Part III: Classes and Metaclasses by Sahand Saba работали Алексей Пирогов и Дмитрий Дементий. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».
Дмитрий Дементий
6 лет назад