Метаклассы, дескрипторы (__get__, __set__), __init_subclass__
Метакласс — это класс классов. Дескриптор — объект, управляющий доступом к атрибуту. Оба механизма лежат в основе Django ORM, SQLAlchemy, Pydantic, dataclasses.
Когда 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__ — настройка пространства имёнfrom collections import OrderedDict
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases):
# Возвращается dict для хранения namespace при разборе тела класса
# Используется когда важен порядок объявления (Python 3.7+ dict сохраняет порядок)
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'] — в порядке объявления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:
# 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"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__ при определении класса.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.