Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Метаклассы и дескрипторы
metaclasses

Метаклассы и дескрипторы

Метаклассы, дескрипторы (__get__, __set__), __init_subclass__

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Метаклассы и дескрипторы в Python

  • Маршрут: Устройство среды выполнения · тема 3 из 3
  • Навыки: S1, S6
  • До урока: oop, closures_decorators, type_system, python_advanced.
  • Результат: управление созданием классов с доказательством, что более простой механизм недостаточен.
  • Основа: дескрипторы и __init_subclass__. Работа в эксплуатации: реестр, валидация и жизненный цикл класса. Углубление: __prepare__, построение метакласса, конфликты и разрешение атрибутов.
  • Подтверждение: реализация метакласса и равносильный рефакторинг через декоратор или __init_subclass__.

Метакласс участвует в создании класса, а дескриптор управляет доступом к атрибуту. Сначала разберём эти механизмы отдельно, затем посмотрим, как они взаимодействуют при разрешении атрибутов.

#1. Как Python создаёт классы

Когда 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) # True

#2. Создание метакласса

class 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__ по-прежнему полезен, когда нужно:

  • Запретить определение дублирующихся атрибутов (вернуть контейнер с проверкой).
  • Автоматически собирать метаданные о порядке объявления (как в dataclasses).
  • Использовать __prepare__ как точку для логирования или валидации до создания класса.

Историческая справка: до Python 3.7 dict не гарантировал порядок, и OrderedDict в __prepare__ был единственным способом гарантировать, что _field_order отражает порядок объявления, а не произвольный порядок хэш-таблицы.


#3. Практические паттерны с метаклассами

#Автоматическая регистрация классов (плагины)

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]()

#Запрет абстрактных методов (упрощённый ABC)

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'

#Singleton через метакласс

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!

#4. __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"

#5. Дескрипторы

Дескриптор — объект с __get__, __set__ и/или __delete__. Управляет доступом к атрибуту класса.

#Data и Non-data дескрипторы

# 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... — из кэша

#6. Порядок разрешения атрибутов (MRO)

Обычное чтение 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

#7. Когда использовать метаклассы и дескрипторы

ЗадачаРешение
Валидация атрибутовДескриптор (или property)
Вычисляемые поля@property или cached_property
ORM (SQLAlchemy Column)Дескриптор
Автоматическая регистрация__init_subclass__ (проще) или метакласс
SingletonМетакласс (или __new__)
Модификация всех методов классаМетакласс
Валидация структуры класса при определенииМетакласс

Правило: Используйте __init_subclass__ вместо метакласса когда возможно. Метакласс даёт больше контроля, но увеличивает сложность. Если то же самое можно сделать через декоратор класса или __init_subclass__ — используйте их.


#8. Практические примеры из реальных библиотек

#SQLAlchemy (упрощённо)

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"

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

Сначала попробуйте явную регистрацию, декоратор и __init_subclass__. Результат основы — работающий реестр с проверкой конфликта имён и без метакласса, если особый контракт создания класса не нужен.

#Шаг 2. Проверьте поведение

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}

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: добавьте второй обработчик, конфликт, явный сброс реестра для тестов и импорт в разном порядке. Затем реализуйте тот же контракт обычной функцией регистрации и сравните скрытые побочные эффекты.

#Шаг 4. Объясните внутренний механизм

Только после сравнения реализуйте вариант через метакласс с __prepare__ и __new__. Покажите реальный контракт порядка объявления или построения класса, который нельзя выразить проще, и разберите возможный конфликт метаклассов.

#Упражнения

  1. Напишите дескриптор NonNegative проверяющий что значение ≥ 0. Используйте __set_name__ для хранения значения в _fieldname.
  2. Реализуйте метакласс AbstractMeta, который при создании класса проверяет, что все методы помеченные @abstractmethod реализованы.
  3. Создайте @singleton через метакласс. Объясните: почему __call__ метакласса вызывается при MyClass()?
  4. Напишите cached_property дескриптор вручную (без functools). Почему он должен быть non-data дескриптором?
  5. Используя __init_subclass__, создайте систему плагинов: базовый класс Serializer со списком зарегистрированных подклассов. Подклассы должны объявить format: str.
  6. Объясните порядок вызовов Meta.__prepare__, Meta.__new__, Meta.__init__ при определении класса.

#Самопроверка

  • Дескриптор хранит данные в экземпляре, а доступ через класс возвращает сам дескриптор; два поля и два экземпляра не смешивают значения.
  • Перед собственной реализацией AbstractMeta сравните её с abc.ABCMeta и укажите, какую гарантию стандартного механизма вы воспроизводите.
  • Singleton проверяется на аргументах повторного конструктора и потоках; если эти случаи не поддержаны, ограничение записано явно.
  • Cached property остаётся non-data descriptor: после первого доступа значение лежит в 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