Лекция 9. Метаклассы и __init_subclass__#
1. type как метакласс#
Класс — тоже объект. Кто его создаёт? Метакласс.
По умолчанию метакласс — type.
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))
<class 'type'>
<class '__main__.Foo'>
<class 'int'>
<class 'type'>
<class 'type'>
type(name, bases, namespace) — создание класса#
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__]}")
Point2.dim = 2, p.x = 1, p.y = 2
Point2 mro: ['Point2', 'object']
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__: задаёт устройство будущих экземпляров
и создаёт дескрипторы слотов. Посмотрим на пространство имён до и после вызова:
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
class body
before type.__new__: ('x', 'y')
x in namespace: False
after type.__new__: member_descriptor
Дескриптор x уже создан, хотя экземпляров ещё нет. Если __dict__
не унаследован и не указан в __slots__, поддержка словаря экземпляра не добавляется.
Память под поля конкретного объекта выделяется позже; __init__ заполняет их:
traced_point = TracedPoint(1, 2)
print(traced_point.x, traced_point.y)
print(hasattr(traced_point, "__dict__"))
instance init
1 2
False
Изменение кортежа после создания класса не добавляет новые слоты:
TracedPoint.__slots__ = ("x", "y", "extra")
traced_point.extra = 42
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[6], line 2
1 TracedPoint.__slots__ = ("x", "y", "extra")
----> 2 traced_point.extra = 42
AttributeError: 'TracedPoint' object has no attribute 'extra' and no __dict__ for setting new attributes
__new__#
Добавим действие при создании класса: метакласс запишет его в реестр.
Параметр register=False отключит регистрацию:
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"))
registered: ['SlackPlugin', 'EmailPlugin']
register attribute: False
register попадает в RegistryMeta.__new__. Он принят отдельным аргументом и не передаётся дальше в super().__new__.
У SlackPlugin явно передано register=True, у EmailPlugin срабатывает
значение по умолчанию. Параметр не становится атрибутом класса:
register=False у Plugin не наследуется подклассами.
Метакласс перехватил создание подклассов и записал их в реестр. Полезно
для систем плагинов, но здесь __init_subclass__ проще.
__prepare__#
Обычно переопределять __prepare__ не нужно.
Стандартный type.__prepare__
возвращает пустой dict, который заполнится при выполнении тела класса:
namespace = type.__prepare__("Example", ())
print(type(namespace))
print(namespace)
<class 'dict'>
{}
Если в теле класса дважды присвоить значение одному имени, обычный dict сохранит только второе.
Через __prepare__ подставим словарь,
который проверяет каждую запись во время выполнения тела класса:
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)
preparing Settings
creating Settings
8000
Повторное присваивание падает во время выполнения тела класса:
class BrokenSettings(metaclass=UniqueMeta):
port: ClassVar[int] = 8000
port = 9000
preparing BrokenSettings
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[10], line 1
----> 1 class BrokenSettings(metaclass=UniqueMeta):
2 port: ClassVar[int] = 8000
3 port = 9000
Cell In[10], line 3, in BrokenSettings()
1 class BrokenSettings(metaclass=UniqueMeta):
2 port: ClassVar[int] = 8000
----> 3 port = 9000
Cell In[9], line 4, in UniqueNames.__setitem__(self, name, value)
2 def __setitem__(self, name: str, value: object) -> None:
3 if name in self:
----> 4 raise TypeError(f"duplicate class attribute: {name}")
5 super().__setitem__(name, value)
TypeError: duplicate class attribute: port
Напечаталось preparing BrokenSettings, но нет creating BrokenSettings:
до __new__ выполнение не дошло. С обычным словарём он получил бы
только port = 9000 и уже не увидел бы повторную запись.
__init__#
В __init__ класс уже создан: доступны его атрибуты и MRO.
Подберём для него логгер по полному имени:
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)
__main__.Task
__main__.ExportTask
same logger: False
Метод __init__ выполнился для Task и для ExportTask, каждому назначен свой логгер.
Экземпляры для этого не нужны. Метод возвращает None; готовый класс возвращает __new__.
__call__#
Упрощённая схема type.__call__ (псевдокод):
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__ запускаем только при первом вызове:
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)
Config __init__ called
same instance: True
{'theme': 'dark'}
Оба имени ссылаются на один объект, поэтому изменение через c1 видно через c2.
Вызов super().__call__ создаёт и инициализирует экземпляр; свой __call__ решает,
нужно ли его вообще вызывать.
Наследование и выбор метакласса#
Метакласс действует и на потомков: повторять metaclass=LoggerMeta не нужно.
При создании внука снова выполнится LoggerMeta.__init__:
class ExportTaskChild(ExportTask):
pass
print(type(Task).__name__)
print(type(ExportTaskChild).__name__)
print(ExportTaskChild.logger.name)
LoggerMeta
LoggerMeta
__main__.ExportTaskChild
Явный metaclass=type не сбрасывает метакласс базы. Python выбирает наиболее
конкретного из совместимых кандидатов:
class ExportWithType(Task, metaclass=type):
pass
print(type(ExportWithType) is LoggerMeta)
True
Конфликт метаклассов#
Попробуем объединить регистрацию из Plugin и логгер из Task:
class LoggedPlugin(Plugin, Task):
pass
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[15], line 1
----> 1 class LoggedPlugin(Plugin, Task):
2 pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
У Plugin метакласс RegistryMeta, у Task — LoggerMeta; они не наследуются
друг от друга. Итоговый метакласс должен быть подклассом метаклассов всех баз,
а Python не создаёт такой класс автоматически.
Указание type не обходит это ограничение:
class StillConflicting(Plugin, Task, metaclass=type):
pass
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[16], line 1
----> 1 class StillConflicting(Plugin, Task, metaclass=type):
2 pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Объединим метаклассы явно и проверим, что оба механизма работают:
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)
['RegisteredLoggerMeta', 'RegistryMeta', 'LoggerMeta', 'type', 'object']
registered: True
logger: __main__.LoggedPlugin
RegistryMeta.__new__ зарегистрировал класс, а LoggerMeta.__init__ назначил
логгер. Если метаклассы переопределяют один и тот же метод, порядок задаёт MRO,
а продолжение цепочки зависит от вызовов super().
3. Готовый метакласс: ABCMeta#
Создадим два класса с одинаковым пространством имён, меняя только метакласс.
Один создаст type, другой — ABCMeta:
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__)
method marker: True
plain metaclass: type
abstract metaclass: ABCMeta
abstract methods: frozenset({'read'})
created: PlainStorage
abstractmethod(read) добавил функции метку __isabstractmethod__.
type её не обрабатывает, а ABCMeta собрал __abstractmethods__:
AbstractStorage()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[19], line 1
----> 1 AbstractStorage()
TypeError: Can't instantiate abstract class AbstractStorage without an implementation for abstract method 'read'
Содержимое пространства имён одинаковое, но с ABCMeta создать экземпляр
не получилось. Наследование от ABC выбирает этот метакласс автоматически.
4. __init_subclass__ — часто проще метакласса#
Соберём тот же реестр через __init_subclass__:
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())}")
registered: ['SlackPlugin', 'EmailPlugin']
__init_subclass__ автоматически работает как метод класса: cls —
создаваемый подкласс. Вызов super() позволяет другим базовым классам
тоже обработать создание подкласса.
Здесь register обрабатывает уже __init_subclass__; остальные аргументы
передаются дальше через kwargs. Если аргумент никто не обработает,
он дойдёт до object.__init_subclass__, который вызовет TypeError:
class UnknownOption(Plugin, unknown=True):
pass
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[21], line 1
----> 1 class UnknownOption(Plugin, unknown=True):
2 pass
Cell In[20], line 7, in Plugin.__init_subclass__(cls, register, **kwargs)
4 def __init_subclass__(
5 cls: type[Plugin], *, register: bool = True, **kwargs: object
6 ) -> None:
----> 7 super().__init_subclass__(**kwargs)
8 if register:
9 Plugin._registry[cls.__name__] = cls
TypeError: UnknownOption.__init_subclass__() takes no keyword arguments
__init_subclass__ наследуется и работает для внуков#
Plugin.__init_subclass__ работает при создании потомков Plugin, а не самого
класса. У внука может не быть собственного __init_subclass__:
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))
registered: ['SlackPlugin', 'EmailPlugin', 'SlackChild', 'InternalChild']
Plugin registered: False
own __init_subclass__: False
Оба внука зарегистрированы. register=False был аргументом при создании
InternalPlugin; при создании InternalChild снова сработало значение
по умолчанию register=True. Аргументы, переданные в __init_subclass__, сами не наследуются.
Переопределение без super() обрывает цепочку#
Переопределим __init_subclass__ в QuietPlugin без вызова super():
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.__init_subclass__: QuietChild
QuietPlugin registered: True
QuietChild registered: False
Сам QuietPlugin ещё зарегистрирован методом Plugin.__init_subclass__.
При создании QuietChild вызван QuietPlugin.__init_subclass__, который
не передал управление дальше: регистрация пропала.
Вернём super() и повторим создание классов:
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)
QuietPlugin.__init_subclass__: QuietChild
QuietChild registered: True
Несколько родителей: MRO и именованные аргументы#
Python выбирает первую реализацию __init_subclass__ по MRO родителей.
Другие реализации вызываются через super(): каждый метод забирает свои
аргументы, а оставшиеся передаёт дальше.
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)
Root.__init_subclass__ Named
Root.__init_subclass__ Tagged
У Event ромб наследования. Посмотрим порядок вызовов __init_subclass__ и MRO:
class Event(Named, Tagged, label="Login", tag="security"):
pass
print(" -> ".join(base.__name__ for base in Event.__mro__))
Named.__init_subclass__ Event Login
Tagged.__init_subclass__ Event security
Root.__init_subclass__ Event
Event -> Named -> Tagged -> Root -> object
Выполняются Named.__init_subclass__, Tagged.__init_subclass__
и Root.__init_subclass__; общая база сработала один раз.
super() внутри Named перешёл в Tagged, хотя непосредственная база
Named — Root: важен MRO создаваемого Event.
При создании внука цепочка повторяется, но аргументы заголовка Event
заново не передаются:
class SpecialEvent(Event):
pass
Named.__init_subclass__ SpecialEvent default
Tagged.__init_subclass__ SpecialEvent none
Root.__init_subclass__ SpecialEvent
При создании 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.
Метакласс применит этот декоратор, добавит имя события и зарегистрирует подкласс GitEvent.
Это учебный способ связать несколько действий при создании класса;
здесь подошёл бы и __init_subclass__.
%%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))
Overwriting /tmp/event_meta_demo.py
!python /tmp/event_meta_demo.py
FileChanged constructor: (path: str = '', lines_added: int = 0) -> None
{"event": "FileChanged", "data": {"path": "README.md", "lines_added": 12}}
event types: ['CommitCreated', 'FileChanged']
Поля path и lines_added объявлены в классе. По ним dataclass(cls)
сгенерировал __init__, который принимает эти аргументы и записывает их в экземпляр.
Подклассам не нужен отдельный декоратор. Реестр связывает имя события из JSON с соответствующим классом.
Что видит тайпчекер#
signature(FileChanged) показывает параметры path и lines_added, но mypy
не выполняет код метакласса. Посмотрим, какие типы он вывел для класса и его поля:
!mypy --strict /tmp/event_meta_demo.py
/tmp/event_meta_demo.py:41: note: Revealed type is "def () -> event_meta_demo.FileChanged"
/tmp/event_meta_demo.py:42: note: Revealed type is "str"
/tmp/event_meta_demo.py:44: error: Unexpected keyword argument "path" for "FileChanged" [call-arg]
/tmp/event_meta_demo.py:44: error: Unexpected keyword argument "lines_added" for "FileChanged" [call-arg]
Found 2 errors in 1 file (checked 1 source file)
Тип поля path известен: str. Но конструктор для mypy имеет вид
def () -> FileChanged, поэтому он отвергает корректный вызов с path= и lines_added=.
Сгенерированный __init__ ему неизвестен.
Как сообщить тайпчекеру о конструкторе#
Обычный @dataclass mypy распознаёт сам, а его вызов внутри EventMeta не анализирует.
Добавим @dataclass_transform() на метакласс:
это сообщает тайпчекеру, что конструктор строится из полей. В сокращённом примере:
%%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 покажет сигнатуру конструктора
Overwriting /tmp/dc_transform_demo.py
!mypy --strict /tmp/dc_transform_demo.py
!python /tmp/dc_transform_demo.py
/tmp/dc_transform_demo.py:24: note: Revealed type is "def (path: str =, lines_added: int =) -> dc_transform_demo.FileChanged"
Success: no issues found in 1 source file
FileChanged(path='README.md', lines_added=12)
Теперь mypy вывел параметры конструктора из полей и принял тот же вызов.
Автору метакласса достаточно поставить декоратор один раз, чтобы тайпчекер
проверял вызовы всех таких классов. Сам __init__ по-прежнему создаёт dataclass(cls);
dataclass_transform только сообщает тайпчекеру правило его построения.
Параметры dataclass_transform#
Посмотрим на сигнатуру декоратора:
from inspect import signature
from typing import dataclass_transform
print(signature(dataclass_transform))
(*, eq_default: bool = True, order_default: bool = False, kw_only_default: bool = False, frozen_default: bool = False, field_specifiers: tuple[type[typing.Any] | typing.Callable[..., typing.Any], ...] = (), **kwargs: Any) -> <class '_IdentityCallable'>
Параметр |
По умолчанию |
Что описывает для тайпчекера |
|---|---|---|
|
|
Генерируется ли сравнение по полям через |
|
|
Генерируются ли |
|
|
Передаются ли все параметры полей в конструктор только по имени. |
|
|
Запрещено ли менять поля после инициализации. |
|
|
Какие функции или классы описывают отдельные поля, например |
|
нет |
Резерв для будущих расширений. |
Суффикс _default означает значение по умолчанию для соответствующего флага eq,
order, kw_only или frozen. Если API библиотеки принимает такой флаг явно,
он может переопределить это значение; обработать его при выполнении должна сама библиотека.
При выполнении декоратор только сохраняет настройки в __dataclass_transform__:
@dataclass_transform(kw_only_default=True, eq_default=False)
class ModelMeta(type):
pass
print(getattr(ModelMeta, "__dataclass_transform__"))
{'eq_default': False, 'order_default': False, 'kw_only_default': True, 'frozen_default': False, 'field_specifiers': (), 'kwargs': {}}
В задаче 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.Изменение готового класса — декоратор класса.