Метаклассы, дескрипторы (__get__, __set__), __init_subclass__
- Маршрут: Устройство среды выполнения · тема 3 из 3
- Навыки:
S1,S6- До урока:
oop,closures_decorators,type_system,python_advanced.- Результат: управление созданием классов с доказательством, что более простой механизм недостаточен.
- Основа: дескрипторы и
__init_subclass__. Работа в эксплуатации: реестр, валидация и жизненный цикл класса. Углубление:__prepare__, построение метакласса, конфликты и разрешение атрибутов.- Подтверждение: реализация метакласса и равносильный рефакторинг через декоратор или
__init_subclass__.
Метакласс участвует в создании класса, а дескриптор управляет доступом к атрибуту. Сначала разберём эти механизмы отдельно, затем посмотрим, как они взаимодействуют при разрешении атрибутов.
Когда Python встречает class Foo:, он вызывает метакласс для создания объекта-класса.
# Это два эквивалентных способа создать класс:
# Способ 1 — обычный синтаксис
class MyClass:
x = 42
def method(self): pass
# Способ 2 — явный вызов type
MyClass = type("MyClass", (), {"x": 42, "method": lambda self: None})
# type(name, bases, dict) создаёт новый класс:
# name — имя класса
# bases — кортеж базовых классов
# dict — пространство имён (атрибуты и методы)
print(type(MyClass)) # <class 'type'>
print(type(int)) # <class 'type'>
print(type(type)) # <class 'type'> — type — метакласс самого себя!object
↑ наследует
int, str, list, MyClass...
type
↑ является экземпляром
int, str, list, MyClass... (type — метакласс всех классов)
type наследует от object
object является экземпляром type
isinstance(int, type) # True — int — экземпляр type
issubclass(int, object) # True — int наследует object
isinstance(type, type) # True
isinstance(type, object) # Trueclass Meta(type):
def __new__(mcs, name, bases, namespace):
# mcs — сам метакласс (Meta)
# name — имя создаваемого класса ("MyClass")
# bases — базовые классы ((object,))
# namespace — dict с методами и атрибутами
print(f"Создаётся класс: {name}")
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace):
# После создания класса
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
# При создании экземпляра: MyClass(...)
print(f"Создаётся экземпляр {cls.__name__}")
instance = super().__call__(*args, **kwargs)
return instance
class MyClass(metaclass=Meta):
pass
obj = MyClass()
# Вывод:
# Создаётся класс: MyClass
# Создаётся экземпляр MyClass__prepare__ — настройка пространства имён__prepare__ вызывается после синтаксического разбора, но до выполнения
тела класса, и возвращает объект,
который будет использоваться как пространство имён (обычно dict). Это
позволяет метаклассу контролировать, как хранятся атрибуты при определении класса.
from collections import OrderedDict
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases):
# Контейнер принимает имена во время выполнения тела класса.
return OrderedDict()
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, dict(namespace))
cls._field_order = list(namespace.keys())
return cls
class DataModel(metaclass=OrderedMeta):
id = None
name = None
email = None
print(DataModel._field_order) # ['id', 'name', 'email'] — в порядке объявленияЗачем это нужно: В Python 3.7+ dict сам сохраняет порядок вставки, поэтому
OrderedDict в __prepare__ больше не нужен для этой цели. Однако __prepare__
по-прежнему полезен, когда нужно:
__prepare__ как точку для логирования или валидации до создания
класса.Историческая справка: до Python 3.7 dict не гарантировал порядок, и
OrderedDict в __prepare__ был единственным способом гарантировать, что
_field_order отражает порядок объявления, а не произвольный порядок хэш-таблицы.
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # не регистрируем базовый класс
plugin_name = namespace.get("name", name.lower())
mcs.registry[plugin_name] = cls
return cls
class BasePlugin(metaclass=PluginMeta):
pass
class CSVPlugin(BasePlugin):
name = "csv"
def process(self, data): ...
class JSONPlugin(BasePlugin):
name = "json"
def process(self, data): ...
# Автоматически зарегистрированы:
print(PluginMeta.registry) # {"csv": CSVPlugin, "json": JSONPlugin}
# Фабричная функция
def get_plugin(name):
return PluginMeta.registry[name]()class StrictMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Проверяем что все требуемые методы реализованы
required = set()
for base in bases:
required |= getattr(base, "_required_methods", set())
for method in required:
if not callable(getattr(cls, method, None)):
raise TypeError(f"{name} must implement '{method}'")
return cls
class Interface(metaclass=StrictMeta):
_required_methods = {"serialize", "deserialize"}
class ConcreteImpl(Interface):
def serialize(self): ...
def deserialize(self): ...
# OK
class BrokenImpl(Interface):
def serialize(self): ...
# TypeError: BrokenImpl must implement 'deserialize'class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self, url):
self.url = url
self.connection = None
db1 = DatabaseConnection("postgresql://...")
db2 = DatabaseConnection("another://...") # параметры игнорируются!
assert db1 is db2 # True — один и тот же объектclass ValidatedMeta(type):
def __new__(mcs, name, bases, namespace):
# Все методы должны начинаться со строчной буквы
for key, value in namespace.items():
if callable(value) and not key.startswith("_"):
if key != key.lower():
raise TypeError(
f"Method '{key}' in {name} must be lowercase"
)
return super().__new__(mcs, name, bases, namespace)
class API(metaclass=ValidatedMeta):
def get_user(self): ... # OK
def GetUser(self): ... # TypeError!__init_subclass__ — лёгкая альтернативаPython 3.6+. Вызывается при создании подкласса — без отдельного метакласса.
class Base:
subclasses = []
def __init_subclass__(cls, register=True, **kwargs):
super().__init_subclass__(**kwargs)
if register:
Base.subclasses.append(cls)
class ChildA(Base):
pass
class ChildB(Base, register=False):
pass
print(Base.subclasses) # [ChildA] — ChildB не зарегистрирован
# Расширенный пример — ORM-подобная регистрация
class Model:
_models = {}
def __init_subclass__(cls, table=None, **kwargs):
super().__init_subclass__(**kwargs)
if table is None:
table = cls.__name__.lower() + "s"
cls._table_name = table
Model._models[table] = cls
class User(Model, table="users"):
pass
class Product(Model):
pass # table = "products" (автоматически)
print(User._table_name) # "users"
print(Product._table_name) # "products"Дескриптор — объект с __get__, __set__ и/или __delete__. Управляет доступом к атрибуту класса.
# Data descriptor: определяет __set__ (и/или __delete__)
# Имеет приоритет над __dict__ экземпляра
# Non-data descriptor: только __get__
# Уступает __dict__ экземпляраclass Descriptor:
def __set_name__(self, owner, name):
# Вызывается при создании класса, знает имя атрибута
self.name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self # доступ через класс: Circle.radius → Descriptor
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
def __delete__(self, obj):
delattr(obj, self.private_name)
class MyClass:
field = Descriptor()
obj = MyClass()
obj.field = 42 # → __set__(obj, 42)
print(obj.field) # → __get__(obj, MyClass) → 42
del obj.field # → __delete__(obj)class TypedField:
def __init__(self, field_type, min_val=None, max_val=None):
self.field_type = field_type
self.min_val = min_val
self.max_val = max_val
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name)
def __set__(self, obj, value):
if not isinstance(value, self.field_type):
raise TypeError(
f"{self.public_name} must be {self.field_type.__name__}, "
f"got {type(value).__name__}"
)
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.public_name} must be >= {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.public_name} must be <= {self.max_val}")
setattr(obj, self.private_name, value)
class Product:
name = TypedField(str)
price = TypedField(float, min_val=0.0)
quantity = TypedField(int, min_val=0, max_val=10000)
p = Product()
p.name = "Widget" # OK
p.price = 9.99 # OK
p.price = -1.0 # ValueError: price must be >= 0.0
p.quantity = "много" # TypeError: quantity must be int, got str@property — это дескрипторclass Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float):
if value < -273.15:
raise ValueError("Ниже абсолютного нуля!")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value: float):
self.celsius = (value - 32) * 5/9
t = Temperature(25)
print(t.fahrenheit) # 77.0
t.fahrenheit = 32 # → celsius = 0
print(t.celsius) # 0.0
t.celsius = -300 # ValueError!cached_property)class cached_property:
"""Вычисляет один раз, затем кэширует в __dict__ экземпляра."""
def __init__(self, func):
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
self.attrname = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Non-data descriptor: если значение уже в __dict__ — возвращает его
if self.attrname in obj.__dict__:
return obj.__dict__[self.attrname]
value = self.func(obj)
obj.__dict__[self.attrname] = value # кэшируем
return value
class Circle:
def __init__(self, radius):
self.radius = radius
@cached_property
def area(self):
import math
print("Вычисляем...")
return math.pi * self.radius ** 2
c = Circle(5)
print(c.area) # Вычисляем... → 78.5398...
print(c.area) # (без "Вычисляем") → 78.5398... — из кэшаОбычное чтение obj.x проходит через object.__getattribute__. Реализация
учитывает MRO и протокол дескрипторов в таком порядке:
# Порядок поиска атрибута obj.x:
# 1. type(obj).__mro__ — ищем data descriptor (с __set__)
# 2. obj.__dict__ — ищем в экземпляре
# 3. type(obj).__mro__ — ищем non-data descriptor или обычный атрибут класса
class DataDesc:
def __get__(self, obj, t): return "data descriptor"
def __set__(self, obj, val): pass # наличие __set__ делает его data descriptor
class NonDataDesc:
def __get__(self, obj, t): return "non-data descriptor"
class MyClass:
data = DataDesc() # data descriptor
non_data = NonDataDesc()
obj = MyClass()
obj.__dict__["data"] = "instance"
obj.__dict__["non_data"] = "instance"
print(obj.data) # "data descriptor" — data descriptor выигрывает
print(obj.non_data) # "instance" — instance __dict__ выигрывает у non-data| Задача | Решение |
|---|---|
| Валидация атрибутов | Дескриптор (или property) |
| Вычисляемые поля | @property или cached_property |
| ORM (SQLAlchemy Column) | Дескриптор |
| Автоматическая регистрация | __init_subclass__ (проще) или метакласс |
| Singleton | Метакласс (или __new__) |
| Модификация всех методов класса | Метакласс |
| Валидация структуры класса при определении | Метакласс |
Правило: Используйте
__init_subclass__вместо метакласса когда возможно. Метакласс даёт больше контроля, но увеличивает сложность. Если то же самое можно сделать через декоратор класса или__init_subclass__— используйте их.
class Column:
"""Дескриптор для колонок таблицы."""
def __init__(self, column_type):
self.column_type = column_type
def __set_name__(self, owner, name):
self.name = name
# Регистрируем колонку в модели
if not hasattr(owner, "_columns"):
owner._columns = {}
owner._columns[name] = self
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
obj.__dict__[self.name] = value
class DeclarativeMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if hasattr(cls, "_columns"):
cls._table_name = name.lower() + "s"
return cls
class Base(metaclass=DeclarativeMeta):
pass
class User(Base):
id = Column("INTEGER")
name = Column("VARCHAR(100)")
email = Column("VARCHAR(255)")
u = User()
u.name = "Alice"
print(User._columns) # {"id": Column, "name": Column, "email": Column}
print(User._table_name) # "users"Сначала попробуйте явную регистрацию, декоратор и __init_subclass__.
Результат основы — работающий реестр с проверкой конфликта имён и без
метакласса, если особый контракт создания класса не нужен.
class Handler:
registry: dict[str, type["Handler"]] = {}
kind: str
def __init_subclass__(cls, *, kind: str, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
if kind in cls.registry:
raise ValueError(f"обработчик {kind!r} уже зарегистрирован")
cls.kind = kind
cls.registry[kind] = cls
class JsonHandler(Handler, kind="json"):
pass
assert Handler.registry == {"json": JsonHandler}Практика с подсказками: добавьте второй обработчик, конфликт, явный сброс реестра для тестов и импорт в разном порядке. Затем реализуйте тот же контракт обычной функцией регистрации и сравните скрытые побочные эффекты.
Только после сравнения реализуйте вариант через метакласс с __prepare__ и
__new__. Покажите реальный контракт порядка объявления или построения класса,
который нельзя выразить проще, и разберите возможный конфликт метаклассов.
NonNegative проверяющий что значение ≥ 0. Используйте __set_name__ для хранения значения в _fieldname.AbstractMeta, который при создании класса проверяет, что все методы помеченные @abstractmethod реализованы.@singleton через метакласс. Объясните: почему __call__ метакласса вызывается при MyClass()?cached_property дескриптор вручную (без functools). Почему он должен быть non-data дескриптором?__init_subclass__, создайте систему плагинов: базовый класс Serializer со списком зарегистрированных подклассов. Подклассы должны объявить format: str.Meta.__prepare__, Meta.__new__, Meta.__init__ при определении класса.AbstractMeta сравните её с abc.ABCMeta и
укажите, какую гарантию стандартного механизма вы воспроизводите.instance.__dict__ и перекрывает __get__.format, а порядок __prepare__ → выполнение тела класса → __new__ → __init__ подтверждён журналом.Закрепите тему в лаборатории
«Реестр обработчиков: функция, __init_subclass__ и метакласс».
Вы сравните три механизма регистрации и оставите метакласс только для контракта,
который действительно требует вмешательства во время создания класса.
git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git
cd python-labs
uv sync --group test
uv run --group test pytest metaclasses/tests