Перейти к основному содержимому

Метапрограммирование

📺 Слайды к лекции

Метапрограммирование — когда программа создаёт, изменяет или анализирует свой код (или код других программ) во время выполнения. В большинстве языков это экзотика. В Python — повседневная реальность: декораторы, property, dataclass, Django ORM — всё это метапрограммирование. Чтобы понять, как эти инструменты работают (и уметь писать свои), придётся спуститься на уровень ниже: к объектной модели Python, дескрипторам и метаклассам.

Python Data Model — всё есть объект

Objects are Python's abstraction for data. All data in a Python program is represented by objects or by relations between objects. Every object has an identity, a type and a value.Python Docs

Фраза «в Python всё — объект» звучит банально, но за ней стоит конкретная архитектура. Каждое значение — число, строка, функция, класс, модуль — это структура в памяти интерпретатора.

PyObject и PyTypeObject в CPython

На уровне C-реализации интерпретатора каждый объект начинается со структуры PyObject:

cpython/Include/object.h (упрощённо)
typedef struct _object {
Py_ssize_t ob_refcnt; // счётчик ссылок
PyTypeObject *ob_type; // указатель на тип
} PyObject;

Тип объекта — тоже объект. Он наследуется от PyObject и называется PyTypeObject. Там лежит информация о поведении типа: слоты для __init__, __str__, __hash__ и т.д. Некоторые слоты обязательные, некоторые — нет.

Наследование типов работает и на уровне C. Например, int — это PyTypeObject, а bool наследуется от int:

type_hierarchy.py
print(bool.__bases__)     # (<class 'int'>,)
print(isinstance(True, int)) # True
print(True + True) # 2 — bool ведёт себя как int

Identity, type, value

У каждого объекта в Python три характеристики:

ХарактеристикаЧто этоКак проверить
IdentityУникальный идентификатор (адрес в памяти в CPython)id(obj)
TypeТип объекта — определяет допустимые значения и операцииtype(obj)
ValueЗначение объектаЗависит от типа
identity_type_value.py
x = 42
print(id(x)) # адрес в памяти (например, 140234866835760)
print(type(x)) # <class 'int'>
print(x) # 42 — значение

# Identity проверяется оператором is
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — одинаковые значения
print(a is b) # False — разные объекты в памяти
Важная деталь

Identity объекта неизменна на протяжении его жизни. Тип объекта тоже, как правило, не меняется (хотя технически можно присвоить obj.__class__, это почти никогда не нужно). Значение может меняться — для мутабельных объектов.

Классы, экземпляры и type

Класс = шаблон, экземпляр = реализация

Класс — чертёж, по которому строится экземпляр. Экземпляр — конкретный объект, созданный по этому чертежу:

class_instance.py
class MyClass:
def __init__(self, param: int):
self.param = param

def increment(self) -> int:
return self.param + 1

myclass = MyClass(123) # экземпляр
print(myclass.increment()) # 124

Класс — тоже объект

А вот интересная часть: класс — тоже объект. У него есть тип, он хранится в памяти, его можно передавать как аргумент. Какой тип у класса?

class_is_object.py
class Foobar:
pass

print(type(Foobar)) # <class 'type'>

foo = Foobar()
print(type(foo)) # <class '__main__.Foobar'>

Экземпляр foo — объект типа Foobar. А сам Foobar — объект типа type. Получается цепочка: экземпляр → класс → метакласс.

isinstance: цепочка instance → class → metaclass

isinstance проверяет принадлежность объекта к типу. Та же цепочка работает и для классов:

isinstance_chain.py
class Foobar:
pass

foo = Foobar()

print(isinstance(foo, Foobar)) # True — foo экземпляр Foobar
print(isinstance(Foobar, type)) # True — Foobar экземпляр type
print(isinstance(type, type)) # True — type экземпляр самого себя!

Эту цепочку можно визуализировать:

Стрелки instance of означают type(x) is Y, а subclass of — наследование (__bases__). Обратите внимание: type замыкается сам на себя -- это единственный объект в Python, который является экземпляром самого себя.

Двойная роль type()

type() в Python играет две роли — и это часто путает:

С одним аргументом — возвращает тип объекта:

type_one_arg.py
x = 1
print(type(x)) # <class 'int'>
print(type(type(x))) # <class 'type'>
print(type(type(type(x)))) # <class 'type'> — дальше всегда type

type — сам себе метакласс. Мета-мета-классов в Python нет — цепочка заканчивается на type.

С тремя аргументами — создаёт новый класс:

type_create.py
# Обычное определение класса
class Dog:
sound = "Woof"
def speak(self):
return self.sound

# Эквивалент через type()
Dog = type('Dog', (object,), {
'sound': 'Woof',
'speak': lambda self: self.sound,
})

d = Dog()
print(d.speak()) # Woof
Определение

type(name, bases, dict) — конструктор классов. name — имя класса, bases — кортеж базовых классов, dict — словарь атрибутов и методов. Любой класс Python создаётся именно этим механизмом — оператор class под капотом вызывает type().

Механизм создания классов

Когда Python встречает оператор class, происходят четыре шага:

class_creation.py
class C:
def f(self):
print('abc')
X = 1
  1. Исполняется тело класса — создаются f, X и складываются в словарь (namespace)
  2. Идентифицируется метаклассtype по умолчанию, или тот, что указан в metaclass=
  3. Вызывается метакласс: C = type('C', (object,), {'f': f, 'X': X, ...})
  4. type выполняет внутренние регистрации, устанавливает __qualname__, __doc__ и т.п.
type_create_explicit.py
# Создание класса вручную — то, что делает оператор class
MyClass = type('MyClass', (), {})
print(MyClass) # <class '__main__.MyClass'>

Дескрипторы

Прежде чем разбирать поиск атрибутов и метаклассы, разберёмся с дескрипторами. На них построены property, classmethod, staticmethod и даже обычные методы.

Протокол дескрипторов

Дескриптор — класс, реализующий один или несколько методов из протокола:

descriptor_protocol.py
# Протокол дескрипторов
class Descriptor:
def __get__(self, obj, objtype=None):
"""Вызывается при чтении атрибута"""
...

def __set__(self, obj, value):
"""Вызывается при записи атрибута"""
...

def __delete__(self, obj):
"""Вызывается при удалении атрибута"""
...

Если объект реализует хотя бы __get__, он считается дескриптором. Python вызывает эти методы автоматически при доступе к атрибутам.

property как полный дескриптор

property — самый известный дескриптор. Реализует все три метода протокола:

property_descriptor.py
class PosDataModel:
def __init__(self):
self._params = []

@property
def params(self):
return self._params

@params.setter
def params(self, new_params):
assert all(map(lambda p: p > 0, new_params))
self._params = new_params

@params.deleter
def params(self):
del self._params

model = PosDataModel()
model.params = [0.1, 0.5, 0.4]
print(model.params) # [0.1, 0.5, 0.4]
del model.params

Data vs non-data дескрипторы

Дескрипторы делятся на две категории, и разница между ними напрямую влияет на порядок поиска атрибутов:

ТипКакие методы реализуетПриоритет
Data descriptor__get__ + (__set__ и/или __delete__)Выше, чем instance.__dict__
Non-data descriptorТолько __get__Ниже, чем instance.__dict__

Если у атрибута экземпляра и data-дескриптора одно имя, побеждает дескриптор. Если у атрибута экземпляра и non-data дескриптора одно имя, побеждает атрибут экземпляра.

Почему это важно

На этом различии построен cached_property: он кеширует результат, записывая его в instance.__dict__, и при следующем обращении Python находит значение в __dict__ раньше, чем дескриптор. Работает только потому, что cached_property — non-data дескриптор.

cached_property как non-data дескриптор

cached_property.py
class cached_property:
"""Упрощённая реализация functools.cached_property"""
def __init__(self, func):
self.func = func

def __get__(self, obj, cls):
if obj is None:
return self
value = self.func(obj)
# Записываем в __dict__ экземпляра.
# При следующем обращении Python найдёт значение в __dict__
# раньше, чем вызовет __get__ — потому что non-data дескриптор
# имеет меньший приоритет.
obj.__dict__[self.func.__name__] = value
return value

Поскольку cached_property не реализует __set__, это non-data дескриптор. Значение из instance.__dict__ имеет больший приоритет, и __get__ больше не вызывается.

Функции как non-data дескрипторы

А теперь — неожиданный факт: обычные функции в Python — это non-data дескрипторы. Именно так работает механизм bound methods:

function_descriptor.py
def foo(x):
return x

# У функции есть метод __get__!
print(foo.__get__) # <method-wrapper '__get__' of function object ...>

# Когда __get__ вызывается с экземпляром, создаётся bound method
f = foo.__get__(92, int)
print(f) # <bound method foo of 92>
print(f()) # 92

Когда вы пишете obj.method(), Python находит функцию method в __dict__ класса, вызывает method.__get__(obj, type(obj)), получает bound method — и вызывает его. Вот почему self передаётся автоматически.

Поиск атрибутов

Теперь, зная про дескрипторы, разберём, как Python ищет атрибуты. Этот механизм определяет, что произойдёт при любом обращении к obj.attr.

Поиск атрибута у экземпляра

Когда вы пишете instance.foobar, Python выполняет примерно следующий алгоритм через __getattribute__:

attribute_lookup.py
# Псевдокод поиска атрибута instance.foobar
def __getattribute__(instance, name):
# 1. Ищем в классе (и его MRO) data descriptor
for cls in type(instance).__mro__:
if name in cls.__dict__:
attr = cls.__dict__[name]
if hasattr(attr, '__set__') or hasattr(attr, '__delete__'):
# Data descriptor — высший приоритет
return attr.__get__(instance, type(instance))

# 2. Ищем в instance.__dict__
if name in instance.__dict__:
return instance.__dict__[name]

# 3. Ищем в классе non-data descriptor или обычный атрибут
for cls in type(instance).__mro__:
if name in cls.__dict__:
attr = cls.__dict__[name]
if hasattr(attr, '__get__'):
return attr.__get__(instance, type(instance))
return attr

# 4. Не нашли — вызываем __getattr__ (если определён)
raise AttributeError(name)

Порядок приоритетов:

  1. Data descriptor в классе (например, property)
  2. instance.dict — атрибуты экземпляра
  3. Non-data descriptor или обычный атрибут класса
  4. __getattr__ — fallback, если ничего не найдено

Алгоритм в виде диаграммы:

Три случая в действии

Рассмотрим поиск атрибутов на конкретном примере:

three_cases.py
class A:
X = 92 # простой атрибут класса

@property
def managed(self): # data descriptor (property)
return self._managed

def foo(self): # non-data descriptor (функция)
print(self.y)

def __init__(self, y):
self.y = y # атрибут экземпляра

a = A(62)

Случай 1: a.y -- атрибут экземпляра

  1. type(a).__getattribute__(a, 'y') запускает алгоритм
  2. Ищем 'y' в A.__dict__ (по MRO) -- не находим data descriptor с таким именем
  3. Ищем 'y' в a.__dict__ -- находим значение 62
  4. Возвращаем 62

Случай 2: a.X -- обычный атрибут класса

  1. type(a).__getattribute__(a, 'X') запускает алгоритм
  2. Ищем 'X' в A.__dict__ -- находим 92, но у int нет __set__/__delete__ -- это не data descriptor
  3. Ищем 'X' в a.__dict__ -- не находим
  4. Снова проверяем A.__dict__['X'] -- у 92 нет __get__, это не дескриптор вообще
  5. Возвращаем 92 напрямую из Class.__dict__

Случай 3: a.foo -- non-data descriptor (bound method)

  1. type(a).__getattribute__(a, 'foo') запускает алгоритм
  2. Ищем 'foo' в A.__dict__ -- находим функцию, но у функции нет __set__ -- не data descriptor
  3. Ищем 'foo' в a.__dict__ -- не находим
  4. Снова проверяем A.__dict__['foo'] -- у функции есть __get__ -- это non-data descriptor
  5. Вызываем foo.__get__(a, A) -- возвращается bound method

Случай 4: a.managed -- data descriptor (property)

  1. type(a).__getattribute__(a, 'managed') запускает алгоритм
  2. Ищем 'managed' в A.__dict__ -- находим property объект, у которого есть __get__, __set__, __delete__
  3. Это data descriptor -- сразу вызываем managed.__get__(a, A), даже не заглядывая в a.__dict__
  4. Property вызывает getter-функцию, возвращается self._managed
Ключевое наблюдение

Data descriptor (property) побеждает instance.__dict__ -- даже если в a.__dict__ есть ключ 'managed', property перехватит обращение. Non-data descriptor (функция) проигрывает instance.__dict__ -- именно поэтому работает cached_property.

Разница между a.foo и A.foo

instance_vs_class.py
class A:
X = 92
def __init__(self, y):
self.y = y
def foo(self):
print(self.y)

a = A(62)
a.y # ищем в instance.__dict__ → находим
a.X # нет в instance.__dict__ → ищем в class.__dict__ → находим
a.foo # нет в instance.__dict__ → находим функцию в class.__dict__
# → вызываем foo.__get__(a, A) → получаем bound method

При A.foo (обращение через класс, а не экземпляр) __get__ вызывается с obj=None, и возвращается сама функция:

class_access.py
print(A.foo)    # <function A.foo at 0x...> — обычная функция
print(a.foo) # <bound method A.foo of <A object>> — bound method

Почему? Метод function.__get__ проверяет первый аргумент: если obj is None, возвращает саму функцию, иначе -- оборачивает в bound method.

Методы хранятся в dict класса

Следствие: методы не копируются в каждый экземпляр. Они живут в __dict__ класса, а instance.__dict__ содержит только данные экземпляра:

dict_location.py
class A:
def f(self):
pass

a = A()
print(a.__dict__) # {} — пусто!
print('f' in A.__dict__) # True — метод хранится в классе

Поиск атрибутов у класса

При обращении к атрибуту класса (например, MyClass.attr) поиск идёт через метакласс. Класс -- экземпляр метакласса, поэтому работает аналогичный алгоритм, но на уровень выше: роль «экземпляра» играет сам класс, а роль «класса» -- метакласс.

class_attribute_lookup.py
# Псевдокод поиска атрибута Class.foobar
def metaclass_getattribute(Class, name):
Metaclass = type(Class)

# 1. Ищем data descriptor в метаклассе (по MRO метакласса)
for meta in Metaclass.__mro__:
if name in meta.__dict__:
attr = meta.__dict__[name]
if hasattr(attr, '__set__') or hasattr(attr, '__delete__'):
return attr.__get__(Class, Metaclass)

# 2. Ищем в Class.__dict__ (по MRO класса)
for cls in Class.__mro__:
if name in cls.__dict__:
attr = cls.__dict__[name]
if hasattr(attr, '__get__'):
# Любой дескриптор (data или non-data) — вызываем __get__
return attr.__get__(None, Class)
return attr

# 3. Ищем non-data descriptor в метаклассе
for meta in Metaclass.__mro__:
if name in meta.__dict__:
attr = meta.__dict__[name]
if hasattr(attr, '__get__'):
return attr.__get__(Class, Metaclass)
return attr

# 4. Fallback
raise AttributeError(name)

Диаграмма поиска атрибута у класса:

Ключевое отличие от поиска у экземпляра

При поиске у экземпляра дескрипторы в классе делятся на data и non-data, и между ними вклинивается instance.__dict__. При поиске у класса любой дескриптор в Class.__dict__ (data или non-data) вызывается через __get__ -- разделение на data/non-data применяется только к дескрипторам метакласса.

Именно поэтому A.foo вызывает foo.__get__(None, A) и возвращает функцию -- хотя foo это non-data descriptor, при обращении через класс он всё равно проходит через __get__.

Метаклассы

Мы разобрали дескрипторы, поиск атрибутов и двойную роль type(). Теперь — к метаклассам.

[Metaclasses] are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why). — Tim Peters, автор timsort и плодовитый контрибьютор Python

Что такое метакласс

Метакласс — это:

  • Класс классов. Как класс определяет поведение экземпляров, так метакласс определяет поведение классов.
  • Фабрика классов. Создаёт и настраивает классы.
  • По умолчанию метакласс всех классов — type.

Создание своего метакласса

Метакласс наследует от type:

metaclass.py
class Meta(type):
def __new__(mcs, name, bases, namespace):
print(f"Creating class: {name}")
# Можно модифицировать namespace перед созданием класса
cls = super().__new__(mcs, name, bases, namespace)
return cls

def __init__(cls, name, bases, namespace):
print(f"Initializing class: {name}")
super().__init__(name, bases, namespace)

class MyClass(metaclass=Meta):
x = 42

# Creating class: MyClass
# Initializing class: MyClass

Чтобы использовать метакласс, передайте его в metaclass= при определении класса.

Специальные методы метаклассов

У метаклассов четыре специальных метода:

МетодКогда вызываетсяЧто делает
__prepare__(name, bases)До исполнения тела классаВозвращает объект для namespace (dict-like)
__new__(mcs, name, bases, namespace)При создании объекта классаСоздаёт и возвращает новый класс
__init__(cls, name, bases, namespace)После создания объекта классаИнициализирует класс (по аналогии с __init__ обычных классов)
__call__(cls, *args, **kwargs)При вызове класса для создания экземпляраКонтролирует создание экземпляров

__call__ и «круглые скобочки»

Когда вы пишете B(), Python не просто вызывает B.__init__. На самом деле вызывается type(B).__call__(B) — метод __call__ метакласса:

call_equivalence.py
class B:
def __call__(self, *args, **kwds):
print(*args, **kwds)

b = B() # эквивалентно: type(B).__call__(B)
b("hello") # эквивалентно: type(b).__call__(b, "hello")

Принцип простой: оператор вызова () всегда проходит через __call__ типа объекта, а не самого объекта.

Создание экземпляра: что происходит при MyClass()

Когда вы вызываете MyClass(), цепочка вызовов выглядит так:

  1. Metaclass.__call__(MyClass, *args, **kwargs) -- метакласс перехватывает вызов
  2. Внутри __call__ вызывается MyClass.__new__(MyClass, *args, **kwargs) -- создаётся объект
  3. Если __new__ вернул экземпляр MyClass, вызывается MyClass.__init__(instance, *args, **kwargs) -- инициализация
  4. Возвращается готовый экземпляр
instance_creation.py
class TrackedMeta(type):
def __call__(cls, *args, **kwargs):
print(f"1. Metaclass.__call__ для {cls.__name__}")
instance = cls.__new__(cls, *args, **kwargs)
print(f"2. {cls.__name__}.__new__ вернул {instance}")
if isinstance(instance, cls):
cls.__init__(instance, *args, **kwargs)
print(f"3. {cls.__name__}.__init__ завершён")
return instance

class Tracked(metaclass=TrackedMeta):
def __new__(cls, *args, **kwargs):
return super().__new__(cls)

def __init__(self, value):
self.value = value

t = Tracked(42)
# 1. Metaclass.__call__ для Tracked
# 2. Tracked.__new__ вернул <__main__.Tracked object at 0x...>
# 3. Tracked.__init__ завершён

Создание класса: prepare --> тело --> new --> init

При создании самого класса (не экземпляра, а именно класса) цепочка другая:

  1. Metaclass.__prepare__(name, bases) -- подготавливает namespace (словарь, в котором будет исполняться тело класса)
  2. Исполняется тело класса -- все определения попадают в namespace
  3. Metaclass.__new__(mcs, name, bases, namespace) -- создаёт объект класса
  4. Metaclass.__init__(cls, name, bases, namespace) -- инициализирует класс
  5. Вызывается __set_name__ для всех дескрипторов в namespace
  6. Вызывается __init_subclass__ у родительского класса

__prepare__ — подготовка namespace

__prepare__ вызывается перед исполнением тела класса и должен вернуть dict-like объект:

prepare.py
def prepare_class(name, bases, metaclass=None, **kwargs):
"""Псевдокод: как Python вызывает __prepare__"""
if metaclass is None:
metaclass = compute_default_metaclass(bases)
prepare = getattr(metaclass, '__prepare__', None)
if prepare is not None:
return prepare(name, bases, **kwargs)
else:
return dict()

Зачем это нужно? Исторически __prepare__ использовали для возврата OrderedDict, чтобы сохранить порядок определения атрибутов (в Python ≤ 3.6 обычный dict не гарантировал порядок). С Python 3.7 словари упорядочены по умолчанию, но __prepare__ по-прежнему полезен для продвинутых сценариев — например, для автоматической регистрации атрибутов при добавлении.

Метаклассы как вызываемые объекты

Метаклассом может быть любой вызываемый объект, не обязательно класс:

callable_metaclass.py
class Base:
pass

class A(
Base,
foo=92,
metaclass=lambda *args, **kwargs: print(args, kwargs),
):
def foo(self):
pass

# Выведет:
# ('A', (<class '__main__.Base'>,),
# {'__module__': '__main__', '__qualname__': 'A',
# 'foo': <function A.foo at 0x...>})
# {'foo': 92}

В этом примере метакласс-lambda получает имя класса, кортеж базовых классов, namespace и дополнительные keyword-аргументы. Конечно, на практике так не делают — это демонстрация механики.

Типичный метакласс (шаблон)

typical_metaclass.py
from collections import OrderedDict

class Meta(type):
@classmethod
def __prepare__(metacls, name, bases, **kwargs):
# Типичный пример для Python <= 3.7
return OrderedDict()

def __new__(cls, name, bases, attrs, **kwargs):
# Здесь можно модифицировать attrs перед созданием класса
return super().__new__(cls, name, bases, attrs)

def __init__(cls, name, bases, attrs, **kwargs):
# Аналог декоратора класса — настройка после создания
super().__init__(name, bases, attrs)
О prepare

Метод __prepare__ чаще всего реализуют как @classmethod, поскольку он вызывается до создания экземпляра метакласса (т.е. до создания самого класса).

Ограничения метаклассов

Два важных правила:

  1. Только один метакласс в иерархии наследования. Если два родительских класса используют разные метаклассы, Python выдаст TypeError. Метакласс потомка должен быть подклассом метаклассов всех родителей.

  2. Метаклассы наследуются. Если Base использует Meta, то class Child(Base) тоже будет использовать Meta — указывать metaclass= повторно не нужно.

metaclass_inheritance.py
class Meta(type):
pass

class Base(metaclass=Meta):
pass

class Child(Base): # автоматически использует Meta
pass

print(type(Child)) # <class '__main__.Meta'>
Конфликт метаклассов
class MetaA(type): pass
class MetaB(type): pass

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass

# class C(A, B): pass # TypeError: metaclass conflict

Решение — создать метакласс, наследующий от обоих: class MetaC(MetaA, MetaB): pass.

__init_subclass__ — альтернатива метаклассам

PEP 487 добавил в Python 3.6 хук __init_subclass__ — вызывается при создании подкласса. В большинстве случаев проще и удобнее метакласса.

Как это работает

init_subclass.py
class Philosopher:
def __init_subclass__(cls, /, default_name, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_name = default_name

class AustralianPhilosopher(Philosopher, default_name="Bruce"):
pass

print(AustralianPhilosopher.default_name) # Bruce

Первый аргумент cls — это не класс, в котором определён __init_subclass__, а вновь созданный подкласс. Keyword-аргументы, переданные в class Child(Parent, key=value), попадают в __init_subclass__.

Про @classmethod

__init_subclass__ неявно является classmethod — декоратор @classmethod применять не нужно.

Plugin registry pattern

Самый частый паттерн — автоматическая регистрация плагинов:

plugin_registry.py
class Plugin:
_registry = {}

def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__.lower()
Plugin._registry[name] = cls
print(f"Registered plugin: {name}")

class JSONPlugin(Plugin, plugin_name="json"):
pass

class XMLPlugin(Plugin, plugin_name="xml"):
pass

print(Plugin._registry)
# {'json': <class 'JSONPlugin'>, 'xml': <class 'XMLPlugin'>}

Когда достаточно __init_subclass__, а когда нужен метакласс

Практический совет

В 90% случаев __init_subclass__ достаточно вместо метаклассов.

ЗадачаИнструмент
Валидация подклассов__init_subclass__
Авто-регистрация плагинов__init_subclass__
Установка атрибутов на подклассах__init_subclass__
Модификация namespace до создания классаМетакласс (__prepare__)
Контроль создания экземпляров (Singleton)Метакласс (__call__)
Перехват type.__new__ для модификации классаМетакласс

Метаклассы нужны, когда надо вмешаться в сам процесс создания класса — модифицировать namespace, контролировать __call__ или перехватить __new__.

Декораторы классов

Ещё одна альтернатива метаклассам — декоратор класса. Функция, которая принимает класс и возвращает модифицированный класс:

class_decorator.py
def add_repr(cls):
def __repr__(self):
attrs = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())
return f'{cls.__name__}({attrs})'
cls.__repr__ = __repr__
return cls

@add_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

print(Point(1, 2)) # Point(x=1, y=2)

Декоратор класса — самый простой инструмент метапрограммирования. @dataclass — хороший пример: добавляет __init__, __repr__, __eq__ и другие методы без всяких метаклассов.

Разница с метаклассом: декоратор применяется после создания класса, а метакласс управляет процессом создания. Декоратор не наследуется (если не применить его к каждому подклассу вручную), а метакласс — наследуется.

__set_name__ — протокол именования дескрипторов

PEP 487 также добавил __set_name__ — метод, который Python вызывает на дескрипторе при создании класса-владельца. Решает классическую проблему: дескриптор хочет знать, под каким именем его сохранили, но при создании имя не передаётся.

set_name.py
class TypeChecked:
def __set_name__(self, owner, name):
self.name = name # автоматически получает имя атрибута!

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError(f"{self.name} must be int, got {type(value)}")
instance.__dict__[self.name] = value

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)

class Config:
max_retries = TypeChecked() # __set_name__ вызовется с name="max_retries"
timeout = TypeChecked() # __set_name__ вызовется с name="timeout"

c = Config()
c.max_retries = 3 # OK
# c.timeout = "fast" # TypeError: timeout must be int, got <class 'str'>

До __set_name__ приходилось передавать имя вручную: max_retries = TypeChecked("max_retries") — дублирование и источник ошибок.

__class_getitem__ — параметризованные типы

PEP 560 добавил __class_getitem__ — вызывается при синтаксисе Class[params]. Через него можно создавать параметризованные типы без метакласса:

class_getitem.py
class Matrix:
def __class_getitem__(cls, params):
rows, cols = params
print(f"Matrix[{rows}, {cols}]")
return cls

# Использование в аннотациях типов
def process(m: Matrix[3, 3]) -> Matrix[3, 1]:
...

Именно так работают list[int], dict[str, int] и другие generic-типы в стандартной библиотеке с Python 3.9+.

Оператор type (Python 3.12+, PEP 695)

В Python 3.12 появился новый синтаксис для определения type aliases и generic-типов — оператор type:

pep695.py
# До Python 3.12
from typing import TypeAlias
Vector: TypeAlias = list[float]

# Python 3.12+
type Vector = list[float]

# Generic-функции и классы — новый синтаксис
def first[T](items: list[T]) -> T:
return items[0]

class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)

Оператор type создаёт ленивый alias — выражение справа вычисляется только при обращении к __value__. А синтаксис [T] заменяет TypeVar — параметры типа определяются прямо в сигнатуре. Под капотом Python создаёт объекты TypeVar и TypeAliasType, но весь механизм работает через __type_params__ — ещё один пример метапрограммирования, встроенного в язык.

Абстрактные метаклассы (ABCMeta)

ABCMeta — метакласс из стандартной библиотеки для абстрактных базовых классов. Хороший пример метакласса «в дикой природе»:

abcmeta.py
from abc import ABCMeta, abstractmethod

class Serializable(metaclass=ABCMeta):
@abstractmethod
def serialize(self) -> bytes:
...

@abstractmethod
def deserialize(self, data: bytes):
...

# Serializable() # TypeError: нельзя создать экземпляр абстрактного класса

ABCMeta перехватывает создание экземпляра в __call__ и проверяет, что все @abstractmethod реализованы. Ещё поддерживает виртуальные подклассы через register().

Современная альтернатива

Начиная с Python 3.4, можно использовать class Serializable(ABC) вместо metaclass=ABCMeta — класс ABC использует ABCMeta внутри.

Практические примеры

Singleton через call

Метакласс контролирует создание экземпляров через __call__:

singleton.py
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 Database(metaclass=SingletonMeta):
def __init__(self):
self.connection = "connected"

db1 = Database()
db2 = Database()
print(db1 is db2) # True — один и тот же экземпляр

SingletonMeta.__call__ перехватывает каждый вызов Database() и возвращает закешированный экземпляр вместо создания нового.

ORM-style синтаксис

С метаклассами можно сделать декларативный синтаксис в стиле Django ORM. Тут сходятся все темы лекции — дескрипторы, __set_name__ и метакласс:

orm_meta.py
class Field:
def __init__(self, field_type):
self.field_type = field_type

def __set_name__(self, owner, name):
self.name = name

def __set__(self, instance, value):
if not isinstance(value, self.field_type):
raise TypeError(
f"{self.name}: expected {self.field_type.__name__}, "
f"got {type(value).__name__}"
)
instance.__dict__[self.name] = value

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)

class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
fields = {
k: v for k, v in namespace.items()
if isinstance(v, Field)
}
cls = super().__new__(mcs, name, bases, namespace)
cls._fields = fields
return cls

class Model(metaclass=ModelMeta):
_fields: dict = {}

def __repr__(self):
attrs = ', '.join(
f'{name}={getattr(self, name, None)!r}'
for name in self._fields
)
return f'{type(self).__name__}({attrs})'

class User(Model):
name = Field(str)
age = Field(int)

u = User()
u.name = "Alice"
u.age = 30
print(u) # User(name='Alice', age=30)

# u.age = "thirty" # TypeError: age: expected int, got str
  • Field — дескриптор с __set_name__ для автоматического получения имени
  • ModelMeta — метакласс, который собирает все Field в _fields
  • Model — базовый класс с __repr__ на основе _fields

Enum как пример метакласса в stdlib

Enum в стандартной библиотеке использует метакласс EnumType (ранее назывался EnumMeta):

enum_metaclass.py
from enum import Enum

class Title(Enum):
MR = 1
MRS = 2
MS = 3

print(type(Title)) # <class 'enum.EnumType'>
print(type(Enum)) # <class 'enum.EnumType'>

EnumType использует __prepare__ для отслеживания порядка членов, __new__ для создания экземпляров-членов, и запрещает наследование от Enum-классов с членами. Без метакласса такой API было бы очень неудобно реализовать.

Django models

Django ORM — пожалуй, самый известный пример метапрограммирования в Python:

django_model.py
from django.db import models

class Student(models.Model):
name = models.CharField(max_length=70)

s = Student(name='Alexey')
print(s.name) # 'Alexey' — строка, а не CharField!

За этим синтаксисом стоит ModelBase — метакласс, который:

  • Собирает все поля (дескрипторы) из определения класса
  • Настраивает связи между моделями (ForeignKey, ManyToMany)
  • Создаёт Meta-опции (имя таблицы, ordering и т.д.)
  • Регистрирует модель в реестре приложений

Когда использовать (и когда НЕ использовать)

Частая ошибка

Метаклассы — сложный инструмент. Не используйте их без необходимости. Код с метаклассами сложнее читать, дебажить и поддерживать. Если задачу можно решить проще — решайте проще.

ЗадачаИнструмент
Генерация __repr__, __eq__, __init__@dataclass
Валидация при создании подклассов__init_subclass__
Авто-регистрация плагинов__init_subclass__
Добавление методов/атрибутов к классуДекоратор класса
Автоименование дескрипторов__set_name__
Контроль создания экземпляров (Singleton)Метакласс (__call__)
ORM-style декларативный синтаксисМетакласс (__new__)
Модификация namespace до создания классаМетакласс (__prepare__)

Правило: @dataclass__init_subclass__ → декоратор класса → метакласс. Идите от простого к сложному и останавливайтесь, как только задача решена.

Порядок создания класса (summary)

Финальная сводка — что происходит при class MyClass(Base, metaclass=Meta)::

  1. MRO разрешены — Python вычисляет порядок разрешения методов (C3 linearization)
  2. Определён метакласс — из metaclass=, наследования или type по умолчанию
  3. __prepare__ — метакласс подготавливает namespace (dict-like объект)
  4. Выполнено тело класса — все определения попадают в namespace
  5. Создан объект классаMetaclass.__new__Metaclass.__init__
  6. __set_name__ — вызывается для всех дескрипторов, определённых в теле класса
  7. __init_subclass__ — вызывается на родительских классах
creation_order.py
class Meta(type):
@classmethod
def __prepare__(metacls, name, bases, **kwargs):
print("1. __prepare__")
return super().__prepare__(name, bases, **kwargs)

def __new__(mcs, name, bases, namespace, **kwargs):
print("3. Meta.__new__")
return super().__new__(mcs, name, bases, namespace)

def __init__(cls, name, bases, namespace, **kwargs):
print("4. Meta.__init__")
super().__init__(name, bases, namespace)

class Base(metaclass=Meta):
def __init_subclass__(cls, **kwargs):
print("5. __init_subclass__")
super().__init_subclass__(**kwargs)

print("--- Создаём Child ---")
class Child(Base):
print("2. Тело класса")

# --- Создаём Child ---
# 1. __prepare__
# 2. Тело класса
# 3. Meta.__new__
# 4. Meta.__init__
# 5. __init_subclass__

Полезные ссылки