Метаклассы, type и __init_subclass__
Классы — тоже объекты
В 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 называют метаклассом — «класс, экземплярами которого являются классы».
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): ...
- Определяется метакласс: явный
metaclass=...или метакласс родителей (обычноtype). Meta.__prepare__создаёт пространство имён (по умолчанию пустой словарь).- Выполняется тело класса как обычный код — результатом заполняется словарь (
total,pay). - Вызывается
Meta("Order", (Base,), namespace)→Meta.__new__иMeta.__init__создают объект класса. - Срабатывают
__set_name__у дескрипторов и__init_subclass__у родителя. - Имя
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
Сделайте базовый класс 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}
class Plugin:
registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[name or cls.__name__.lower()] = cls
Создайте класс 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)
def init(self, x, y):
self.x, self.y = x, y
Point = type("Point", (), {"__init__": init, "norm": lambda self: abs(self.x) + abs(self.y)})
Что выведет print(type(type), type(int), type(42))?
Какой метод метакласса вызывается при создании экземпляра класса Cls()?
Когда стоит писать собственный метакласс?
Войдите, чтобы отмечать пройденные уроки.