0Pricing
Python Academy · Урок

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

Используйте __new__ и __init__ в метаклассах для настройки создания классов.

«Написание пользовательских метаклассов» — бесплатный урок Python Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Python Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Python Academy содержит 4 уроков всего.

Объявление метакласса

Передайте metaclass=MyMeta в определении класса, чтобы использовать специальный метакласс. Метакласс должен наследоваться от type.

class MyMeta(type):
    pass

class Foo(metaclass=MyMeta):
    pass

print(type(Foo))   # <class '__main__.MyMeta'>

Переопределение __new__

__new__(mcs, name, bases, namespace) создаёт объект класса. Используйте его, чтобы изменить пространство имён до построения класса.

class UpperAttrMeta(type):
    def __new__(mcs, name, bases, namespace):
        upper = {k.upper(): v for k, v in namespace.items()
                 if not k.startswith("_")}
        return super().__new__(mcs, name, bases, {**namespace, **upper})

class Config(metaclass=UpperAttrMeta):
    debug = True

print(Config.DEBUG)  # True

Переопределение __init__

__init__(cls, name, bases, namespace) выполняется после создания класса. Используйте его для настройки после создания, например для регистрации.

_registry = {}

class AutoRegisterMeta(type):
    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if bases:   # skip the base class itself
            _registry[name] = cls

class Handler(metaclass=AutoRegisterMeta): pass
class JSONHandler(Handler): pass
class XMLHandler(Handler):  pass

print(_registry)  # {'JSONHandler': ..., 'XMLHandler': ...}

Обеспечение требований интерфейса

Вызывайте TypeError в __new__, если в теле класса отсутствуют обязательные методы.

class InterfaceMeta(type):
    REQUIRED = frozenset({"process", "validate"})

    def __new__(mcs, name, bases, ns):
        if bases and not mcs.REQUIRED.issubset(ns):
            missing = mcs.REQUIRED - ns.keys()
            raise TypeError(f"{name} must define: {missing}")
        return super().__new__(mcs, name, bases, ns)

class Worker(metaclass=InterfaceMeta):
    def process(self): ...
    def validate(self): ...

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

Переопределите __call__ в метаклассе, чтобы перехватить создание экземпляра и каждый раз возвращать один и тот же экземпляр.

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *a, **kw):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*a, **kw)
        return cls._instances[cls]

class DB(metaclass=SingletonMeta):
    pass

print(DB() is DB())  # True

Динамическое добавление методов

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

class MethodInjectorMeta(type):
    def __new__(mcs, name, bases, ns):
        ns["describe"] = lambda self: f"I am {type(self).__name__}"
        return super().__new__(mcs, name, bases, ns)

class Widget(metaclass=MethodInjectorMeta):
    pass

print(Widget().describe())   # I am Widget

__prepare__ для упорядоченного пространства имён

Переопределите __prepare__, чтобы предоставить специальное отображение в качестве пространства имён класса, например для сохранения порядка определения или обнаружения дубликатов.

class NoDuplicatesMeta(type):
    class _NS(dict):
        def __setitem__(self, k, v):
            if k in self and not k.startswith("_"):
                raise ValueError(f"Duplicate attribute: {k}")
            super().__setitem__(k, v)

    @classmethod
    def __prepare__(mcs, name, bases):
        return mcs._NS()

Разрешение конфликтов метаклассов

Если у нескольких базовых классов разные метаклассы, Python требует явно использовать наиболее производный метакласс, иначе возникает ошибка конфликта.

class MetaA(type): pass
class MetaB(type): pass

# class Bad(A, B): pass  # TypeError: metaclass conflict

class MetaAB(MetaA, MetaB): pass   # resolve manually

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
class Good(A, B, metaclass=MetaAB): pass

Метакласс и __init_subclass__

Для регистрации подклассов и простых обработчиков __init_subclass__ проще и не требует метакласса.

class Base:
    _registry = []
    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        Base._registry.append(cls)

class ChildA(Base): pass
class ChildB(Base): pass

print(Base._registry)  # [ChildA, ChildB]

Отслеживание доступа к атрибутам

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

class TraceMeta(type):
    def __getattribute__(cls, name):
        val = super().__getattribute__(name)
        if not name.startswith("_"):
            print(f"Accessing class attr: {name}")
        return val

class Config(metaclass=TraceMeta):
    HOST = "localhost"

_ = Config.HOST   # logs: Accessing class attr: HOST

Шаблон сбора полей ORM

ORM в стиле Django используют метаклассы для сбора дескрипторов Field из тела класса в реестр _meta.

class Field:
    def __init__(self, col): self.col = col

class ModelMeta(type):
    def __new__(mcs, name, bases, ns):
        fields = {k: v for k, v in ns.items() if isinstance(v, Field)}
        ns["_fields"] = fields
        return super().__new__(mcs, name, bases, ns)

class User(metaclass=ModelMeta):
    name = Field("name")
    email = Field("email")

print(User._fields.keys())  # dict_keys(['name', 'email'])

Быстрая проверка

Какой метод в специальном метаклассе вызывается для создания самого объекта класса (до вызова __init__)?

Итоги

Специальные метаклассы переопределяют __new__/__init__, чтобы управлять созданием классов: обеспечивать требования, автоматически регистрировать подклассы, внедрять методы или собирать дескрипторы полей. Для простых задач регистрации предпочитайте __init_subclass__.

Часто задаваемые вопросы

Урок «Написание пользовательских метаклассов» бесплатный?

Да — полный текст урока «Написание пользовательских метаклассов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Python Academy, подпишись на CoddyKit PRO. Курс Python Academy содержит 4 уроков всего.

Чему я научусь в уроке «Написание пользовательских метаклассов»?

Используйте __new__ и __init__ в метаклассах для настройки создания классов. Ты практикуешь Python Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Python Academy?

Предыдущий опыт не требуется. Python Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Написание пользовательских метаклассов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Python Academy?

Да. Каждый урок Python Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Как создаются классы Python
  2. Написание пользовательских метаклассов
  3. Дескрипторы: __get__, __set__, __delete__
  4. __slots__ и оптимизация памяти
← Назад к Python Academy