Реклама
Селектел, перетяжка, 22.06
Селектел, перетяжка, 22.06
Селектел, перетяжка, 22.06

Метаклассы в Python: как работает фабрика классов

Метакласс — это класс класса. Поняв эту идею, вы лучше разберётесь в устройстве Python и сможете выбирать между метаклассами, наследованием и декораторами.

Обложка: Метаклассы в Python: как работает фабрика классов

Если класс в Python — это шаблон, по которому создаются объекты, то метакласс — это шаблон, по которому создаются сами классы. Идея звучит рекурсивно, но именно в ней скрывается ответ на вопрос, почему в Python всё — объект: даже определение класса можно собрать, изменить или зарегистрировать программно.

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

Класс — тоже объект

В Python 3 любой класс является экземпляром метакласса. По умолчанию этим метаклассом выступает type. Поэтому запись class Foo: pass интерпретатор превращает примерно в такой вызов:

			Foo = type('Foo', (), {})
		

С точки зрения языка разницы почти нет: и в том, и в другом случае получается объект класса Foo, у которого есть атрибуты, методы и собственный тип. Проверить это можно прямо в интерпретаторе:

			class Foo:
    pass

print(type(Foo))      # <class 'type'>
print(type(type))     # <class 'type'>
print(Foo.__class__)  # <class 'type'>
		

type сам является экземпляром себя — это одна из тех особенностей Python, которые сначала удивляют, а потом помогают понять, что классы и объекты в языке живут по одним правилам.

Ключевые выводы

Метакласс — это класс, экземплярами которого являются другие классы.

В Python 3 по умолчанию метаклассом любого класса является type.

Вызов type(name, bases, dict) создаёт класс динамически.

Собственный метакласс наследуется от type и переопределяет __new__ или __init__.

Чаще всего задачу решают наследованием или декоратором класса — метакласс нужен редко.

type — не только функция проверки типа

Всем знаком вызов type(x), который возвращает тип объекта. Но если передать type три аргумента, он превращается в фабрику классов:

			NewClass = type('NewClass', (BaseClass,), {'attr': 42})
		
  • первый аргумент — имя класса (становится __name__);
  • второй — кортеж базовых классов (становится __bases__);
  • третий — словарь пространства имён (становится __dict__).

Этот механизм лежит в основе любого определения класса. Когда Python видит ключевое слово class, он сначала собирает тело класса в словарь, а затем вызывает метакласс, чтобы создать сам класс.

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

			def say_hello(self):
    return f"Привет, {self.name}!"

Person = type(
    'Person',
    (),
    {
        '__init__': lambda self, name: setattr(self, 'name', name),
        'hello': say_hello,
    }
)

p = Person('Анна')
print(p.hello())  # Привет, Анна!
		

Такой код редко пишут в production, но он наглядно показывает, что класс — это всего лишь объект, созданный вызовом фабрики. А значит, эту фабрику можно заменить на свою.

Как написать свой метакласс

Чтобы изменить процесс создания класса, наследуемся от type и переопределяем __new__. Этот метод отвечает за создание самого класса, поэтому в нём можно добавить общие атрибуты, проверить имя или зарегистрировать класс в каталоге.

			class AutoAttrMeta(type):
    def __new__(mcs, name, bases, namespace):
        namespace['version'] = 1
        return super().__new__(mcs, name, bases, namespace)


class Plugin(metaclass=AutoAttrMeta):
    pass

print(Plugin.version)  # 1
		

Здесь AutoAttrMeta автоматически добавляет атрибут version каждому классу, который её использует. То же самое можно сделать через наследование, но метакласс действует на этапе создания класса, а не при вызове методов.

Помимо __new__, часто переопределяют __init__ метакласса: он получает уже созданный класс и может его донастроить. А __call__ контролирует создание экземпляров класса — именно он вызывает __new__ и __init__ объекта.

А точно нужен метакласс?

Тим Петерс, автор «Дзена Python», как-то сказал, что метаклассы — это магия, которую 99% разработчиков никогда не понадобится. Если вы сомневаетесь, нужны ли они вам, — скорее всего, не нужны.

Метаклассы — это более глубокая магия, чем та, о которой 99% пользователей должны беспокоиться. Если вы сомневаетесь, нужны ли они вам, значит, не нужны.
Tim Petersавтор «Дзена Python»

Для сравнения — три способа дать классам общий атрибут:

Наследование

			class Base:
    version = 1

class Plugin(Base):
    pass

print(Plugin.version)  # 1
		

Декоратор класса

			def add_version(cls):
    cls.version = 1
    return cls

@add_version
class Plugin:
    pass

print(Plugin.version)  # 1
		

Метакласс

			class VersionMeta(type):
    def __new__(mcs, name, bases, namespace):
        namespace['version'] = 1
        return super().__new__(mcs, name, bases, namespace)

class Plugin(metaclass=VersionMeta):
    pass

print(Plugin.version)  # 1
		

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

Когда метакласс действительно уместен:
— автоматическая регистрация подклассов в плагиновой системе;
— валидация объявлений полей (например, ORM проверяют типы атрибутов);
— принудительный единый API для большого семейства классов;
— реализация паттернов вроде Singleton на уровне класса.

Практический пример: автоматическая регистрация плагинов

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

			class PluginMeta(type):
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != 'BasePlugin':
            mcs.registry[name] = cls
        return cls


class BasePlugin(metaclass=PluginMeta):
    pass


class S3Storage(BasePlugin):
    pass


class LocalStorage(BasePlugin):
    pass


print(BasePlugin.__class__.registry)
# {'S3Storage': <class '__main__.S3Storage'>, 'LocalStorage': <class '__main__.LocalStorage'>}
		

Такой подход удобен, потому что автор плагина просто объявляет класс, а реестр обновляется сам. В реальных фреймворках — например, в старых версиях Django — похожая идея используется для построения моделей данных.

Часто задаваемые вопросы
1
Что такое метакласс в Python?

Метакласс — это класс, экземплярами которого являются другие классы. В Python 3 любой класс по умолчанию создаётся метаклассом type.

2
Как создать класс без ключевого слова class?

С помощью встроенной функции type(name, bases, dict). Первый аргумент — имя, второй — кортеж базовых классов, третий — словарь атрибутов и методов.

3
Когда стоит использовать метакласс?

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

4
Можно ли изменить type напрямую?

Нет, встроенный type — неизменяемый объект. Именно поэтому для кастомизации используют собственный метакласс, унаследованный от type.

5
Есть ли метаклассы в других языках?

Да, но по-разному. Например, в Ruby объекты класса Class сами являются экземплярами Class, а в Smalltalk метаклассы — часть объектной модели. В Python концепция ближе всего к Smalltalk.

Выводы

Метаклассы — не ежедневный инструмент, но понимание их работы делает код Python прозрачнее. Вы начинаете видеть, что class — это тоже объект, а type — всего лишь фабрика, которую при желании можно заменить.

Главное правило: если задачу решает наследование или декоратор класса, не тащите метакласс. А если без контроля за созданием класса никуда — смело берите type и наследуйтесь от него.

Источник и дополнительное чтение: Python Metaclasses на Real Python.