---
jupytext:
  formats: md:myst
  text_representation:
    extension: .md
    format_name: myst
kernelspec:
  display_name: Python 3.14
  language: python
  name: python3
---

```{code-cell} python
:tags: [remove-cell]

import os, sys
os.environ["PATH"] = f"{os.path.dirname(sys.executable)}:{os.environ['PATH']}"
```

# Лекция 9. Метаклассы и `__init_subclass__`

## 1. `type` как метакласс

**Класс — тоже объект.** Кто его создаёт? Метакласс.

По умолчанию метакласс — [`type`](https://docs.python.org/3.14/library/functions.html#type).

```{code-cell} python
from typing import ClassVar

class Foo:
    x: ClassVar[int] = 1

print(type(Foo))
print(type(Foo()))
print(type(42))
print(type(int))
print(type(type))
```

### `type(name, bases, namespace)` — создание класса

```{code-cell} python
from typing import Protocol, cast

# Создадим класс обычным способом:
class Point:
    dim: ClassVar[int] = 2

    def __init__(self, x: int, y: int) -> None:
        self.x, self.y = x, y

class PointShape(Protocol):
    dim: ClassVar[int]
    x: int
    y: int

    def __init__(self, x: int, y: int) -> None: ...

# Теперь создадим похожий класс через type:
def _init(self: PointShape, x: int, y: int) -> None:
    self.x, self.y = x, y

Point2 = cast(
    "type[PointShape]",
    type(
        "Point2",  # имя класса
        (),  # базовые классы; пустой кортеж означает наследование от object
        {"dim": 2, "__init__": _init},  # пространство имён класса
    ),
)

p = Point2(1, 2)
print(f"Point2.dim = {Point2.dim}, p.x = {p.x}, p.y = {p.y}")
print(f"Point2 mro: {[c.__name__ for c in Point2.__mro__]}")
```

---

## 2. Собственный метакласс

Наследуемся от `type`, переопределяем нужные методы.

### Что можно переопределить

- `__prepare__(name, bases, **kwargs)` — подготовка пространства имён **до** выполнения тела класса.
- `__new__(mcs, name, bases, namespace, **kwargs)` — создание объекта класса.
  Возвращает готовый класс.
- `__init__(cls, name, bases, namespace, **kwargs)` — инициализация класса
  (после `__new__`).
- `__call__(cls, *args, **kwargs)` — поведение при `Foo(a, b)`, обычно создание экземпляра.

Для собственной реализации `__prepare__` здесь показана сигнатура без декоратора.
С `@classmethod` первым параметром добавляется `mcs`:
`__prepare__(mcs, name, bases, **kwargs)`.
У `__new__` декоратор не нужен: Python автоматически делает его статическим методом
и передаёт `mcs` при создании класса.

В первых трёх методах `**kwargs` собирает дополнительные именованные аргументы
из `class`, например `register=False`.
Сам `metaclass` туда не попадает: Python использует его для выбора метакласса.
Каждый из этих методов получает аргументы заголовка заново.

У `__call__` это уже аргументы вызова `Foo(...)`, например `timeout=5` в `Foo(timeout=5)`.

`__prepare__`, `__new__` и `__init__` участвуют в создании **класса**.
Метод метакласса `__call__` вызывается позже, когда мы пишем `Foo(...)`.

### `__new__` — когда обрабатывается `__slots__`

В теле класса `__slots__` — обычное присваивание кортежа. После выполнения
тела этот кортеж попадает в `namespace`, переданный метаклассу.

Его обрабатывает `type.__new__`: задаёт устройство будущих экземпляров
и создаёт дескрипторы слотов. Посмотрим на пространство имён до и после вызова:

```{code-cell} python
class SlotsMeta(type):
    def __new__(
        mcs: type[SlotsMeta],
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> SlotsMeta:
        print("before type.__new__:", namespace.get("__slots__"))
        print("x in namespace:", "x" in namespace)
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        print("after type.__new__:", type(vars(cls)["x"]).__name__)
        return cls

class TracedPoint(metaclass=SlotsMeta):
    print("class body")
    __slots__ = ("x", "y")

    def __init__(self, x: int, y: int) -> None:
        print("instance init")
        self.x, self.y = x, y
```

Дескриптор `x` уже создан, хотя экземпляров ещё нет. Если `__dict__`
не унаследован и не указан в `__slots__`, поддержка словаря экземпляра не добавляется.
Память под поля конкретного объекта выделяется позже; `__init__` заполняет их:

```{code-cell} python
traced_point = TracedPoint(1, 2)
print(traced_point.x, traced_point.y)
print(hasattr(traced_point, "__dict__"))
```

Изменение кортежа после создания класса не добавляет новые слоты:

```{code-cell} python
:tags: [raises-exception]

TracedPoint.__slots__ = ("x", "y", "extra")
traced_point.extra = 42
```

### `__new__`

Добавим действие при создании класса: метакласс запишет его в реестр.
Параметр `register=False` отключит регистрацию:

```{code-cell} python
class RegistryMeta(type):
    _registry: ClassVar[dict[str, type[object]]] = {}

    def __new__(
        mcs: type[RegistryMeta],
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        *,
        register: bool = True,
        **kwargs: object,
    ) -> RegistryMeta:
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        if register:
            RegistryMeta._registry[name] = cls
        return cls

class Plugin(metaclass=RegistryMeta, register=False):
    pass

class SlackPlugin(Plugin, register=True):
    channel: ClassVar[str] = "general"

class EmailPlugin(Plugin):
    from_addr: ClassVar[str] = "noreply@example.com"

class InternalPlugin(Plugin, register=False):
    pass

print(f"registered: {list(RegistryMeta._registry.keys())}")
print("register attribute:", hasattr(Plugin, "register"))
```

`register` попадает в `RegistryMeta.__new__`. Он принят отдельным аргументом и не передаётся дальше в `super().__new__`.

У `SlackPlugin` явно передано `register=True`, у `EmailPlugin` срабатывает
значение по умолчанию. Параметр не становится атрибутом класса:
`register=False` у `Plugin` не наследуется подклассами.

Метакласс перехватил создание подклассов и записал их в реестр. Полезно
для систем плагинов, но здесь `__init_subclass__` проще.

### `__prepare__`

Обычно переопределять `__prepare__` не нужно.
Стандартный [`type.__prepare__`](https://docs.python.org/3.14/reference/datamodel.html#preparing-the-class-namespace)
возвращает пустой `dict`, который заполнится при выполнении тела класса:

```{code-cell} python
namespace = type.__prepare__("Example", ())
print(type(namespace))
print(namespace)
```

Если в теле класса дважды присвоить значение одному имени, обычный `dict` сохранит только второе.
Через [`__prepare__`](https://peps.python.org/pep-3115/) подставим словарь,
который проверяет каждую запись во время выполнения тела класса:

```{code-cell} python
class UniqueNames(dict[str, object]):
    def __setitem__(self, name: str, value: object) -> None:
        if name in self:
            raise TypeError(f"duplicate class attribute: {name}")
        super().__setitem__(name, value)

class UniqueMeta(type):
    @classmethod
    def __prepare__(
        mcs: type[UniqueMeta],
        name: str,
        bases: tuple[type[object], ...],
        /,
        **kwargs: object,
    ) -> UniqueNames:
        print(f"preparing {name}")
        return UniqueNames()

    def __new__(
        mcs: type[UniqueMeta],
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> UniqueMeta:
        print(f"creating {name}")
        return super().__new__(mcs, name, bases, namespace, **kwargs)

class Settings(metaclass=UniqueMeta):
    port: ClassVar[int] = 8000

print(Settings.port)
```

Повторное присваивание падает во время выполнения тела класса:

```{code-cell} python
:tags: [raises-exception]

class BrokenSettings(metaclass=UniqueMeta):
    port: ClassVar[int] = 8000
    port = 9000
```

Напечаталось `preparing BrokenSettings`, но нет `creating BrokenSettings`:
до `__new__` выполнение не дошло. С обычным словарём он получил бы
только `port = 9000` и уже не увидел бы повторную запись.

### `__init__`

В `__init__` класс уже создан: доступны его атрибуты и MRO.
Подберём для него логгер по полному имени:

```{code-cell} python
import logging

class LoggerMeta(type):
    logger: logging.Logger

    def __init__(
        cls: LoggerMeta,
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> None:
        super().__init__(name, bases, namespace, **kwargs)
        cls.logger = logging.getLogger(f"{cls.__module__}.{cls.__qualname__}")

class Task(metaclass=LoggerMeta):
    pass

class ExportTask(Task):
    pass

print(Task.logger.name)
print(ExportTask.logger.name)
print("same logger:", Task.logger is ExportTask.logger)
```

Метод `__init__` выполнился для `Task` и для `ExportTask`, каждому назначен свой логгер.
Экземпляры для этого не нужны. Метод возвращает `None`; готовый класс возвращает `__new__`.

### `__call__`

Упрощённая схема `type.__call__` (псевдокод):

```python
def __call__(cls: type[object], *args: object, **kwargs: object) -> object:
    obj = cls.__new__(cls, *args, **kwargs)
    if isinstance(obj, cls):
        type(obj).__init__(obj, *args, **kwargs)
    return obj
```

Свой `__call__` заменяет эту схему целиком и может вернуть уже готовый объект.

Сделаем singleton: повторный вызов `Config()` вернёт объект из кеша.
Стандартную цепочку `__new__` и `__init__` запускаем только при первом вызове:

```{code-cell} python
class SingletonMeta(type):
    _instances: ClassVar[dict[type[object], object]] = {}

    def __call__(cls: SingletonMeta, *args: object, **kwargs: object) -> object:
        if cls not in SingletonMeta._instances:
            SingletonMeta._instances[cls] = super().__call__(*args, **kwargs)
        return SingletonMeta._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self) -> None:
        print("Config __init__ called")
        self.data: dict[str, object] = {}

c1 = Config()
c2 = Config()  # не вызывает __init__ второй раз
print(f"same instance: {c1 is c2}")
c1.data["theme"] = "dark"
print(c2.data)
```

Оба имени ссылаются на один объект, поэтому изменение через `c1` видно через `c2`.
Вызов `super().__call__` создаёт и инициализирует экземпляр; свой `__call__` решает,
нужно ли его вообще вызывать.

### Наследование и выбор метакласса

Метакласс действует и на потомков: повторять `metaclass=LoggerMeta` не нужно.
При создании внука снова выполнится `LoggerMeta.__init__`:

```{code-cell} python
class ExportTaskChild(ExportTask):
    pass

print(type(Task).__name__)
print(type(ExportTaskChild).__name__)
print(ExportTaskChild.logger.name)
```

Явный `metaclass=type` не сбрасывает метакласс базы. Python выбирает наиболее
конкретного из совместимых кандидатов:

```{code-cell} python
class ExportWithType(Task, metaclass=type):
    pass

print(type(ExportWithType) is LoggerMeta)
```

### Конфликт метаклассов

Попробуем объединить регистрацию из `Plugin` и логгер из `Task`:

```{code-cell} python
:tags: [raises-exception]

class LoggedPlugin(Plugin, Task):
    pass
```

У `Plugin` метакласс `RegistryMeta`, у `Task` — `LoggerMeta`; они не наследуются
друг от друга. [Итоговый метакласс должен быть подклассом метаклассов всех баз](https://docs.python.org/3.14/reference/datamodel.html#determining-the-appropriate-metaclass),
а Python не создаёт такой класс автоматически.

Указание `type` не обходит это ограничение:

```{code-cell} python
:tags: [raises-exception]

class StillConflicting(Plugin, Task, metaclass=type):
    pass
```

Объединим метаклассы явно и проверим, что оба механизма работают:

```{code-cell} python
class RegisteredLoggerMeta(RegistryMeta, LoggerMeta):
    pass

class LoggedPlugin(Plugin, Task, metaclass=RegisteredLoggerMeta):
    pass

print([meta.__name__ for meta in type(LoggedPlugin).__mro__])
print("registered:", RegistryMeta._registry["LoggedPlugin"] is LoggedPlugin)
print("logger:", LoggedPlugin.logger.name)
```

`RegistryMeta.__new__` зарегистрировал класс, а `LoggerMeta.__init__` назначил
логгер. Если метаклассы переопределяют один и тот же метод, порядок задаёт MRO,
а продолжение цепочки зависит от вызовов `super()`.

---

## 3. Готовый метакласс: `ABCMeta`

Создадим два класса с одинаковым пространством имён, меняя только метакласс.
Один создаст `type`, другой — [`ABCMeta`](https://docs.python.org/3.14/library/abc.html#abc.ABCMeta):

```{code-cell} python
from abc import ABCMeta, abstractmethod

def read(self: object, key: str) -> bytes:
    raise NotImplementedError()

namespace = {"read": abstractmethod(read)}
PlainStorage = type("PlainStorage", (), namespace)
AbstractStorage = ABCMeta("AbstractStorage", (), namespace)

print("method marker:", getattr(read, "__isabstractmethod__"))
print("plain metaclass:", type(PlainStorage).__name__)
print("abstract metaclass:", type(AbstractStorage).__name__)
print("abstract methods:", AbstractStorage.__abstractmethods__)
print("created:", type(PlainStorage()).__name__)
```

`abstractmethod(read)` добавил функции метку `__isabstractmethod__`.
`type` её не обрабатывает, а `ABCMeta` собрал `__abstractmethods__`:

```{code-cell} python
:tags: [raises-exception]

AbstractStorage()
```

Содержимое пространства имён одинаковое, но с `ABCMeta` создать экземпляр
не получилось. Наследование от `ABC` выбирает этот метакласс автоматически.

---

## 4. `__init_subclass__` — часто проще метакласса

Соберём тот же реестр через `__init_subclass__`:

```{code-cell} python
class Plugin:
    _registry: ClassVar[dict[str, type[Plugin]]] = {}

    def __init_subclass__(
        cls: type[Plugin], *, register: bool = True, **kwargs: object
    ) -> None:
        super().__init_subclass__(**kwargs)
        if register:
            Plugin._registry[cls.__name__] = cls

class SlackPlugin(Plugin): pass
class EmailPlugin(Plugin): pass
class InternalPlugin(Plugin, register=False): pass

print(f"registered: {list(Plugin._registry.keys())}")
```

`__init_subclass__` автоматически работает как метод класса: `cls` —
создаваемый подкласс. Вызов `super()` позволяет другим базовым классам
тоже обработать создание подкласса.

Здесь `register` обрабатывает уже `__init_subclass__`; остальные аргументы
передаются дальше через `kwargs`. Если аргумент никто не обработает,
он дойдёт до `object.__init_subclass__`, который вызовет `TypeError`:

```{code-cell} python
:tags: [raises-exception]

class UnknownOption(Plugin, unknown=True):
    pass
```

### `__init_subclass__` наследуется и работает для внуков

`Plugin.__init_subclass__` работает при создании потомков `Plugin`, а не самого
класса. У внука может не быть собственного `__init_subclass__`:

```{code-cell} python
class SlackChild(SlackPlugin):
    pass

class InternalChild(InternalPlugin):
    pass

print("registered:", list(Plugin._registry))
print("Plugin registered:", "Plugin" in Plugin._registry)
print("own __init_subclass__:", "__init_subclass__" in vars(SlackChild))
```

Оба внука зарегистрированы. `register=False` был аргументом при создании
`InternalPlugin`; при создании `InternalChild` снова сработало значение
по умолчанию `register=True`. Аргументы, переданные в `__init_subclass__`, сами не наследуются.

### Переопределение без `super()` обрывает цепочку

Переопределим `__init_subclass__` в `QuietPlugin` без вызова `super()`:

```{code-cell} python
class QuietPlugin(Plugin):
    def __init_subclass__(cls: type[QuietPlugin], **kwargs: object) -> None:
        print("QuietPlugin.__init_subclass__:", cls.__name__)

class QuietChild(QuietPlugin):
    pass

print("QuietPlugin registered:", "QuietPlugin" in Plugin._registry)
print("QuietChild registered:", "QuietChild" in Plugin._registry)
```

Сам `QuietPlugin` ещё зарегистрирован методом `Plugin.__init_subclass__`.
При создании `QuietChild` вызван `QuietPlugin.__init_subclass__`, который
не передал управление дальше: регистрация пропала.
Вернём `super()` и повторим создание классов:

```{code-cell} python
class QuietPlugin(Plugin):
    def __init_subclass__(cls: type[QuietPlugin], **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)
        print("QuietPlugin.__init_subclass__:", cls.__name__)

class QuietChild(QuietPlugin):
    pass

print("QuietChild registered:", Plugin._registry["QuietChild"] is QuietChild)
```

### Несколько родителей: MRO и именованные аргументы

Python выбирает первую реализацию `__init_subclass__` по MRO родителей.
Другие реализации вызываются через `super()`: каждый метод забирает свои
аргументы, а оставшиеся передаёт дальше.

```{code-cell} python
class Root:
    def __init_subclass__(cls: type[Root], **kwargs: object) -> None:
        print("Root.__init_subclass__", cls.__name__)
        super().__init_subclass__(**kwargs)

class Named(Root):
    def __init_subclass__(
        cls: type[Named], *, label: str = "default", **kwargs: object
    ) -> None:
        print("Named.__init_subclass__", cls.__name__, label)
        super().__init_subclass__(**kwargs)

class Tagged(Root):
    def __init_subclass__(
        cls: type[Tagged], *, tag: str = "none", **kwargs: object
    ) -> None:
        print("Tagged.__init_subclass__", cls.__name__, tag)
        super().__init_subclass__(**kwargs)
```

У `Event` ромб наследования. Посмотрим порядок вызовов `__init_subclass__` и MRO:

```{code-cell} python
class Event(Named, Tagged, label="Login", tag="security"):
    pass

print(" -> ".join(base.__name__ for base in Event.__mro__))
```

Выполняются `Named.__init_subclass__`, `Tagged.__init_subclass__`
и `Root.__init_subclass__`; общая база сработала один раз.
`super()` внутри `Named` перешёл в `Tagged`, хотя непосредственная база
`Named` — `Root`: важен MRO создаваемого `Event`.

При создании внука цепочка повторяется, но аргументы заголовка `Event`
заново не передаются:

```{code-cell} python
class SpecialEvent(Event):
    pass
```

При создании `SpecialEvent` метод `Named.__init_subclass__` получил `label="default"`,
а `Tagged.__init_subclass__` — `tag="none"`. Если `__init_subclass__` сохраняет
настройку в атрибуте класса, этот атрибут наследуется по обычным правилам;
сам аргумент вызова от этого не становится наследуемым.

### Правило

**Если задачу решает `__init_subclass__`, используй его.**
Метакласс нужен, когда требуется:

- Задать пространство имён до выполнения тела класса через `__prepare__`.
- Изменить пространство имён или базовые классы перед вызовом `type.__new__`.
- Управлять созданием экземпляров `cls(...)` через `__call__`.
- Дополнить поведение существующего метакласса, например `ABCMeta`.

---

## 5. Пример: события Git в JSON

Представим сведения о коммите и изменённом файле объектами с разными полями.
У каждого будет общий метод `to_json()` для записи в JSON.

Сбор полей и генерацию `__init__` выполняет
[`@dataclass`](https://docs.python.org/3.14/library/dataclasses.html#dataclasses.dataclass).
Метакласс применит этот декоратор, добавит имя события и зарегистрирует подкласс `GitEvent`.
Это учебный способ связать несколько действий при создании класса;
здесь подошёл бы и `__init_subclass__`.

```{code-cell} python
%%writefile /tmp/event_meta_demo.py
from dataclasses import asdict, dataclass, is_dataclass
from inspect import signature
import json
from typing import TYPE_CHECKING, ClassVar, cast

class EventMeta(type):
    registry: ClassVar[dict[str, type[GitEvent]]] = {}

    def __new__(
        mcs: type[EventMeta],
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> type[GitEvent]:
        cls = cast("type[GitEvent]", super().__new__(mcs, name, bases, namespace, **kwargs))
        if bases:
            cls = dataclass(cls)
            cls.event_type = name
            mcs.registry[name] = cls

        return cls

class GitEvent(metaclass=EventMeta):
    event_type: ClassVar[str]

    def to_json(self) -> str:
        if not is_dataclass(self):
            raise TypeError("GitEvent subclasses must be dataclasses")
        return json.dumps({"event": self.event_type, "data": asdict(self)})

class CommitCreated(GitEvent):
    commit_hash: str = ""
    author: str = ""

class FileChanged(GitEvent):
    path: str = ""
    lines_added: int = 0

if TYPE_CHECKING:
    reveal_type(FileChanged)
    reveal_type(FileChanged.path)

file_change = FileChanged(path="README.md", lines_added=12)
print("FileChanged constructor:", signature(FileChanged))
print(file_change.to_json())
print("event types:", list(EventMeta.registry))
```

```{code-cell} python
!python /tmp/event_meta_demo.py
```

Поля `path` и `lines_added` объявлены в классе. По ним `dataclass(cls)`
сгенерировал `__init__`, который принимает эти аргументы и записывает их в экземпляр.

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

### Что видит тайпчекер

`signature(FileChanged)` показывает параметры `path` и `lines_added`, но `mypy`
не выполняет код метакласса. Посмотрим, какие типы он вывел для класса и его поля:

```{code-cell} python
!mypy --strict /tmp/event_meta_demo.py
```

Тип поля `path` известен: `str`. Но конструктор для `mypy` имеет вид
`def () -> FileChanged`, поэтому он отвергает корректный вызов с `path=` и `lines_added=`.
Сгенерированный `__init__` ему неизвестен.

### Как сообщить тайпчекеру о конструкторе

Обычный `@dataclass` `mypy` распознаёт сам, а его вызов внутри `EventMeta` не анализирует.
Добавим [`@dataclass_transform()`](https://peps.python.org/pep-0681/) на метакласс:
это сообщает тайпчекеру, что конструктор строится из полей. В сокращённом примере:

```{code-cell} python
%%writefile /tmp/dc_transform_demo.py
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast, dataclass_transform

@dataclass_transform()
class EventMeta(type):
    def __new__(
        mcs: type[EventMeta],
        name: str,
        bases: tuple[type[object], ...],
        namespace: dict[str, object],
        **kwargs: object,
    ) -> type[object]:
        cls = cast(type[object], super().__new__(mcs, name, bases, namespace, **kwargs))
        return dataclass(cls)

class FileChanged(metaclass=EventMeta):
    path: str = ""
    lines_added: int = 0

file_change = FileChanged(path="README.md", lines_added=12)
print(file_change)

if TYPE_CHECKING:
    reveal_type(FileChanged)  # mypy покажет сигнатуру конструктора
```

```{code-cell} python
!mypy --strict /tmp/dc_transform_demo.py
!python /tmp/dc_transform_demo.py
```

Теперь `mypy` вывел параметры конструктора из полей и принял тот же вызов.
Автору метакласса достаточно поставить декоратор один раз, чтобы тайпчекер
проверял вызовы всех таких классов. Сам `__init__` по-прежнему создаёт `dataclass(cls)`;
`dataclass_transform` только сообщает тайпчекеру правило его построения.

### Параметры `dataclass_transform`

Посмотрим на [сигнатуру декоратора](https://docs.python.org/3.14/library/typing.html#typing.dataclass_transform):

```{code-cell} python
from inspect import signature
from typing import dataclass_transform

print(signature(dataclass_transform))
```

| Параметр | По умолчанию | Что описывает для тайпчекера |
|---|---|---|
| `eq_default` | `True` | Генерируется ли сравнение по полям через `__eq__`. |
| `order_default` | `False` | Генерируются ли `__lt__`, `__le__`, `__gt__`, `__ge__`. |
| `kw_only_default` | `False` | Передаются ли все параметры полей в конструктор только по имени. |
| `frozen_default` | `False` | Запрещено ли менять поля после инициализации. |
| `field_specifiers` | `()` | Какие функции или классы описывают отдельные поля, например `dataclasses.field`. |
| `**kwargs` | нет | Резерв для будущих расширений. |

Суффикс `_default` означает значение по умолчанию для соответствующего флага `eq`,
`order`, `kw_only` или `frozen`. Если API библиотеки принимает такой флаг явно,
он может переопределить это значение; обработать его при выполнении должна сама библиотека.

При выполнении декоратор только сохраняет настройки в `__dataclass_transform__`:

```{code-cell} python
@dataclass_transform(kw_only_default=True, eq_default=False)
class ModelMeta(type):
    pass

print(getattr(ModelMeta, "__dataclass_transform__"))
```

В задаче `mini_dataclass` эти два аргумента описывают нужный нам интерфейс:

- `kw_only_default=True`: `Point(x=2)` допустим, а `Point(2)` тайпчекер отвергнет.
- `eq_default=False`: сравнение по полям не генерируется. Оператор `==` остаётся доступен;
  в этой задаче объекты сравниваются по идентичности.

Конструктор и `__repr__` в задаче реализуем сами. Сам `dataclass_transform`
не добавляет методы и не обеспечивает выполнение этих правил.

---

## 6. Когда метакласс — правильный ответ

Метакласс оправдан, если нужно вмешаться в выполнение тела класса,
изменить его атрибуты до `type.__new__` или управлять созданием экземпляров при `Cls(...)`.
Например, запрет повторных имён требует проверки записей через `__prepare__`:
после выполнения тела класса обычный словарь уже потерял предыдущее значение.

Для более простых задач достаточно обычных средств:

- Регистрация подклассов — `__init_subclass__`.
- Требование реализовать метод — `ABC` и `abstractmethod`.
- Изменение готового класса — декоратор класса.

## Материалы

- [Создание классов и метаклассы](https://docs.python.org/3.14/reference/datamodel.html#metaclasses)
- [Абстрактные базовые классы — `abc`](https://docs.python.org/3.14/library/abc.html)
- [PEP 3115 — создание классов и `__prepare__`](https://peps.python.org/pep-3115/)
- [PEP 487 — `__init_subclass__` и `__set_name__`](https://peps.python.org/pep-0487/)
- [PEP 681 — описание поведения классов через `dataclass_transform`](https://peps.python.org/pep-0681/)
