Метаклассы, type и __init_subclass__

⏱ 35 мин · 🧩 заданий: 5

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

В Python всё — объект, включая классы. Класс можно положить в переменную, передать в функцию, создать во время выполнения:

class Dog:
    def bark(self):
        return "Гав"

Animal = Dog                       # класс — просто объект в переменной
print(Animal().bark(), Animal is Dog)

def make(cls):
    return cls()                   # класс как аргумент
print(make(Dog).bark())

print(type(Dog), type(Dog()), isinstance(Dog, type))

type: класс всех классов

У type две роли: type(obj) возвращает класс объекта, а type(name, bases, namespace) создаёт новый класс. Тип любого класса — type, поэтому type называют метаклассом — «класс, экземплярами которого являются классы».

42 ──экземпляр──▶ int ──экземпляр──▶ type ◀──┐ └───────┘ (type — экземпляр самого себя) int ──наследует──▶ object ; type ──наследует──▶ object
def greet(self):
    return f"Я {self.name}"

User = type("User", (object,), {"role": "гость", "greet": greet,
                                "__init__": lambda self, name: setattr(self, "name", name)})
u = User("Анна")
print(u.greet(), u.role, User.__mro__)
print(type(type), type(int), type(object))

Что делает инструкция class

class Order(Base, metaclass=Meta):
    total = 0
    def pay(self): ...
  1. Определяется метакласс: явный metaclass=... или метакласс родителей (обычно type).
  2. Meta.__prepare__ создаёт пространство имён (по умолчанию пустой словарь).
  3. Выполняется тело класса как обычный код — результатом заполняется словарь (total, pay).
  4. Вызывается Meta("Order", (Base,), namespace) → Meta.__new__ и Meta.__init__ создают объект класса.
  5. Срабатывают __set_name__ у дескрипторов и __init_subclass__ у родителя.
  6. Имя Order связывается с готовым классом.
print("до класса")
class Demo:
    print("  тело класса выполняется прямо сейчас!")
    x = 1 + 1
    print("  x =", x)
print("после класса", Demo.x)

Свой метакласс

Метакласс наследуется от type и переопределяет его методы:

class Meta(type):
    def __new__(mcls, name, bases, namespace):
        print(f"  создаю класс {name}, атрибуты: {[k for k in namespace if not k.startswith('__')]}")
        # пример: все публичные методы должны иметь docstring
        for key, value in namespace.items():
            if callable(value) and not key.startswith("_") and not value.__doc__:
                raise TypeError(f"{name}.{key}: нужна документация")
        return super().__new__(mcls, name, bases, namespace)

class Service(metaclass=Meta):
    def run(self):
        """Запустить сервис."""

try:
    class BadService(Service):        # метакласс наследуется
        def stop(self):
            pass
except TypeError as e:
    print("TypeError:", e)

Синглтон через call

Cls() — это вызов объекта-класса, поэтому срабатывает type(Cls).__call__, то есть метод метакласса:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            print(f"  создаю {cls.__name__}")
            cls._instances[cls] = super().__call__(*args, **kwargs)  # здесь __new__ + __init__
        return cls._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self):
        self.debug = False

a = Config()
b = Config()
a.debug = True
print(a is b, b.debug)

Синглтон проще

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

Простые альтернативы

init_subclass

Срабатывает у родителя каждый раз, когда создаётся подкласс. Покрывает большинство задач «реестра» и «проверки подклассов» без метаклассов:

class Command:
    commands = {}

    def __init_subclass__(cls, *, name, **kwargs):
        super().__init_subclass__(**kwargs)
        if not hasattr(cls, "execute"):
            raise TypeError(f"{cls.__name__} должен определить execute()")
        Command.commands[name] = cls

class Hello(Command, name="hello"):
    def execute(self):
        return "привет!"

class Bye(Command, name="bye"):
    def execute(self):
        return "пока!"

for cmd in ["hello", "bye"]:
    print(cmd, "→", Command.commands[cmd]().execute())

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

Принимает готовый класс и возвращает его (изменённым) — так устроен @dataclass:

def add_repr(cls):
    def __repr__(self):
        args = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({args})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

print(Point(1, 2))

Где метаклассы используются на практике

Где Зачем
Django Model собирает поля класса в _meta, создаёт менеджеры
SQLAlchemy Declarative превращает атрибуты класса в столбцы таблицы
abc.ABCMeta отслеживает абстрактные методы и запрещает создание
enum.EnumType (раньше EnumMeta) превращает атрибуты в члены перечисления, делает класс итерируемым
from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

print(type(Color), list(Color), Color(2), Color["RED"].value)

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

Вопрос на собеседовании

«Что такое метакласс?» — Класс, экземплярами которого являются классы. По умолчанию это type. Метакласс управляет созданием класса (__new__/__init__) и созданием его экземпляров (__call__). Применяется во фреймворках (Django ORM, ABCMeta, Enum); в прикладном коде обычно хватает __init_subclass__ и декораторов классов.

Итог

  • Классы — объекты; их тип — метакласс type.
  • type(name, bases, dict) создаёт класс; инструкция class делает то же после выполнения тела.
  • Метакласс наследуется от type; __new__ управляет созданием класса, __call__ — созданием экземпляров.
  • Для реестров и проверок подклассов достаточно __init_subclass__, для модификации — декоратора класса.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Сделайте базовый класс Plugin, который через __init_subclass__ автоматически регистрирует каждый подкласс в словаре Plugin.registry по ключу name — параметру класса: class Csv(Plugin, name="csv"). Если name не передан, используйте имя класса в нижнем регистре.

Какие тесты проверяют код (1)
Plugin.registry.clear()
class Csv(Plugin, name="csv"): pass
class JsonExporter(Plugin): pass
assert Plugin.registry == {"csv": Csv, "jsonexporter": JsonExporter}

Задание 2 Программа на Python

Создайте класс Point без инструкции class — вызовом type(...). У него должен быть метод __init__(self, x, y) и метод norm(self), возвращающий abs(x) + abs(y).

Какие тесты проверяют код (2)
assert Point(3, -4).norm() == 7
assert Point.__name__ == "Point" and isinstance(Point, type)
Задание 3 Один вариант ответа

Что выведет print(type(type), type(int), type(42))?

Задание 4 Один вариант ответа

Какой метод метакласса вызывается при создании экземпляра класса Cls()?

Задание 5 Один вариант ответа

Когда стоит писать собственный метакласс?

Войдите, чтобы отмечать пройденные уроки.