# Глава 37. Метаклассы
> Метаклассы — потаенная магия питона, о которой 99% разработчиков не должны даже задумываться.
Тим Питерс, core-контрибьютор питона и автор сортировки Timsort
Как сказал Тим Питерс, если вы задумываетесь, нужно ли вам использовать метаклассы, то нет, не нужно. И это правило железно работает... За исключением того, что о метаклассах нет-нет да спрашивают на собеседованиях. А в дебрях сложных проектов, с которыми приходится сталкиваться, проскальзывает таинственное слово `metaclass`.
Метакласс — мощный инструмент [метапрограммирования.](https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%B0%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) Метакласс — это фабрика классов. С ее помощью классы создаются и кастомизируются прямо в рантайме. Метаклассами пронизана подкапотная работа с классами. Если хотите знать больше, то эта глава для вас.
## Что такое метакласс
Как мы [выяснили](/courses/python/chapters/python_chapter_0180/) в главе про модель данных, все в питоне — это объект. Класс — это объект с типом `type`.
```python {.example_for_playground}
class Dummy:
...
d = Dummy()
print(type(d))
print(type(Dummy))
print(type(type))
```
```
<class '__main__.Dummy'>
<class 'type'>
<class 'type'>
```
Из примера видно, что тип объекта `d` — его класс `Dummy`. Тип класса `Dummy` — `type`. А тип `type` — это `type`. То есть `type` сконструирован так, чтобы являться классом для себя самого. Мы получили зацикленную цепочку из классов.
Вообще `type` относится к классу также, как класс — к своему объекту. Класс нужен, чтобы создавать объекты, а `type` — чтобы создавать классы. А все потому, что `type` является метаклассом.
**Метакласс** — сущность, которая создает классы. И это не уникальная для питона концепция. Метаклассы поддерживаются в Ruby, Objective-C, Perl и других языках.
`type` — это **встроенный метакласс.** Он используется по умолчанию для создания классов. Чтобы понять, как это работает, вспомним, как выглядит работа с классами в питоне.
По месту объявления класса интерпретатор заводит переменную с именем класса. С которой можно работать, как и с любой другой переменной. Но эта переменная (класс) сама может создавать переменные (инстансы). Поэтому она называется классом.
Класс можно присваивать переменной:
```python {.example_for_playground}
class C:
...
x = C
print(type(x))
print(id(x))
print(id(C))
```
```
<class 'type'>
94038844921552
94038844921552
```
Можно добавлять и удалять атрибуты класса.
Что выведет этот код? {.task_text}
```python {.example_for_playground}
class C:
...
C.field = 8
print("field" in C.__dict__)
```
```consoleoutput {.task_source #python_chapter_0370_task_0010}
```
Мы завели пустой класс `C`. Затем добавили в него поле `field` со значением 8. В консоль мы вывели результат проверки, содержится ли поле `field` в словаре с атрибутами. {.task_hint}
```python {.task_answer}
True
```
Класс можно передавать в функцию и возвращать из функции:
```python {.example_for_playground}
def factory(title):
if title == "pancake":
class Pancake:
...
return Pancake
if title == "brownie":
class Brownie:
...
return Brownie
raise ValueError("Unexpected title")
x = factory("brownie")
print(type(x))
print(x.__name__)
```
```
<class 'type'>
Brownie
```
В этом примере мы создаем различные классы прямо внутри функции `factory()` и возвращаем их наружу. Выглядит это странновато. Такой код не очень гибкий, в нем легко запутаться.
Добиться того же самого результата, то есть динамического создания классов, можно с помощью встроенной функции `type()`.
## Встроенный метакласс type и динамическое создание классов
В прошлых главах мы неоднократно пользовались функцией `type()`. Она принимает объект и возвращает его тип.
```python
obj_type = type(obj)
```
В таком варианте использования `type()` фактически возвращает значение dunder-поля `__class__`. Например:
```python {.example_for_playground}
val = 104
print(type(val))
print(val.__class__)
```
```
<class 'int'>
<class 'int'>
```
У `type()` есть и другое применение — динамическое создание классов! В таком случае функция принимает 3 аргумента: имя класса, кортеж его родителей и словарь с атрибутами класса. А возвращает сам класс:
```python
class_obj = type(class_name, parents, attrs)
```
`class_name` превращается в dunder-поле `__name__` нового класса, список родительских классов `parents` становится полем `__bases__`, а словарь `attrs` — не что иное, как `__dict__`.
Пусть вас не смущает, что в зависимости от количества передаваемых аргументов `type()` ведет себя совершенно по-разному: для единственного аргумента возвращает его тип, а для трех аргументов создает объект-класс. Так сложилось по историческим причинам.
Заменим классическое объявление пустого класса на его создание через `type()`.
При заведении класса через `class` объект класса создается автоматически.
```python
class Dummy:
...
```
А при заведении класса через `type()` ответственность за создание объекта лежит на разработчике:
```python
Dummy = type("Dummy", (), {})
```
Обратите внимание: здесь имя класса, переданное строкой в `type()`, совпадает с именем переменной, в которую присваивается созданные класс. В принципе они могут и различаться, но зачем усложнять!
Что выведет этот код? {.task_text}
```python
Dummy = type("Dummy", (), {})
print(len(Dummy.__bases__))
```
```consoleoutput {.task_source #python_chapter_0370_task_0020}
```
Если у класса явно не указан родитель, класс наследуется от `object`. Поэтому кортеж `__bases__` состоит из единственного элемента: `<class 'object'>`. {.task_hint}
```python {.task_answer}
1
```
Атрибутами класса могут быть, разумеется, и поля, и методы. И вот как свободная функция превращается в метод:
```python {.example_for_playground}
def show_summary(self):
print(f"object type: {type(self)}")
print(f"class parents: {type(self).__bases__}")
SimpleClass = type("SimpleClass", (), {"summary": show_summary})
obj = SimpleClass()
obj.summary()
```
```
object type: <class '__main__.SimpleClass'>
class parents: (<class 'object'>,)
```
В этом примере мы завели функцию `show_summary()`. С помощью вызова `type()` создали класс `SimpleClass`, в атрибут `summary` которого превращена функция. Затем мы инстанцировали `obj` от класса `SimpleClass` и вызвали у него данный метод.
Проведите рефакторинг кода: замените объявления `Parent` и `Child` на создание классов через `type()`. {.task_text}
```python {.task_source #python_chapter_0370_task_0030}
class Parent:
def __init__(self):
self.x = 5
def f(self, val):
print(f"Parent f({val})")
return self.x * val
class Child(Parent):
def __init__(self):
self.y = 6
def f(self, val):
print(f"Child f({val})")
return self.x * self.y * val
parent = Parent()
res = parent.f(3)
print(f"Result: {res}\n")
child = Child()
res = child.f(3)
print(f"Result: {res}\n")
```
Создание класса `Parent` после заведения функции `f_parent()`: `Parent = type("Parent", (), {"x": 5, "f": f_parent})`. {.task_hint}
```python {.task_answer}
def f_parent(self, val):
print(f"Parent f({val})")
return self.x * val
def f_child(self, val):
print(f"Child f({val})")
return self.x * self.y * val
Parent = type("Parent", (), {"x": 5, "f": f_parent})
Child = type("Child", (Parent,), {"y": 6, "f": f_child})
parent = Parent()
res = parent.f(3)
print(f"Result: {res}\n")
child = Child()
res = child.f(3)
print(f"Result: {res}\n")
```
Каждый раз, когда в коде мы пишем `class`, срабатывает подкапотная магия питона. Она превращает блок `class` в вызов `type()`. Тело класса исполняется в свежесозданном пространстве имен; имя класса связывается с результатом вызова `type()`.
Метакласс `type` по умолчанию используется при создании классов. Но можно написать и свой собственный метакласс.
## Кастомные метаклассы
Рассмотрим основные шаги, исполняемые при инстанцировании объекта класса:
```python {.example_for_playground}
class Dummy:
...
d = Dummy()
```
Для того чтобы создать инстанс класса, у класса нужно вызвать оператор `()`: он вернет новый объект. Вызов `()` определяется с помощью dunder-метода `__call__()`. Который в свою очередь вызывает конструктор `__new__()` и инициализатор `__init__()`. Если эти методы не объявлены в самом классе, интерпретатор ищет их в родительских классах.
Метод `__new__()` всегда вызывается перед `__init__()`. Он создает объект и возвращает его. А `__init__()` — инициализирует, донастривает переданный в него уже существующий объект. В случае, если создаваемый объект — класс, то для его настройки правильнее использовать `__new__()`.
Если поведение при создании объекта класса вдруг захотелось переопределить, этого можно добиться, присвоив классу новый метод:
```python {.example_for_playground}
class Dummy:
...
def new(cls):
obj = object.__new__(cls)
print(f"Creating object {obj}...")
return obj
Dummy.__new__ = new
d = Dummy()
```
```
Creating object <__main__.Dummy object at 0x7f3fbbf8bad0>...
```
В данном примере мы переписали поведение класса `Dummy` при создании своих инстансов. Но как быть, если мы хотим переопределить поведение при создании **классов?** Как мы уже знаем, `type` — метакласс, по умолчанию создающий классы в питоне. Но вот так просто переопределить его методы `__new__()` и `__init__()` не получится. Любая попытка перезаписи атрибутов `type` завершится исключением:
```
TypeError: can't set attributes of built-in/extension type 'type'
```
Для переопределения действий по созданию класса и нужны кастомные метаклассы.
Для начала определим новый метакласс. Чтобы класс превратился в метакласс, он должен быть наследован от `type` или его потомка.
Затем в определении класса, поведение при создании которого хочется переопределить, укажем кастомный метакласс после ключевого слова `metaclass`.
И вот как это выглядит на примере:
```python {.example_for_playground}
class Meta(type):
def __new__(cls, name, bases, attrs):
obj = super().__new__(cls, name, bases, attrs)
print(f"Creating class {obj}...")
return obj
class Dummy(metaclass=Meta):
...
```
```
Creating class <class '__main__.Dummy'>...
```
Метод `__new__()` метакласса принимает класс, его имя, кортеж родителей и словарь атрибутов.
Реализуйте метакласс `UpperAttrMeta`, который бы переводил все атрибуты класса, не являющиеся dunder-атрибутами, в верхний регистр. {.task_text}
Примените этот метакласс к `SimpleClass`. {.task_text}
```python {.task_source #python_chapter_0370_task_0040}
# Your code here
class SimpleClass:
attr1 = "val1"
attr2 = "val2"
print(hasattr(SimpleClass, "attr1"))
print(hasattr(SimpleClass, "ATTR1"))
```
Метод `__new__()` класса `UpperAttrMeta` принимает аргументы: `cls`, `name`, `bases`, `attrs`. Внутри метода требуется пройтись по ключам и значениям словаря `attrs` и добавить в новый словарь их в модифицированном виде. Затем вызвать `type.__new__()`, передав в него этот новый словарь. {.task_hint}
```python {.task_answer}
class UpperAttrMeta(type):
def __new__(cls, name, bases, attrs):
uppercase_attrs = {
name if name.startswith("__") else name.upper(): value
for name, value in attrs.items()
}
return type.__new__(cls, name, bases, uppercase_attrs)
class SimpleClass(metaclass=UpperAttrMeta):
attr1 = "val1"
attr2 = "val2"
print(hasattr(SimpleClass, "attr1"))
print(hasattr(SimpleClass, "ATTR1"))
```
Метаклассы выполняют довольно простую работу: перехватывают создание класса; модифицируют класс; возвращают уже модифицированный. Вот собственно и все, что нужно знать о метаклассах.
## Советы по использованию метаклассов
Избегайте изменения сигнатур методов через метаклассы, так как это ломает аннотации типов и подсказки от IDE из-за неочевидного поведения классов.
Тестируйте метаклассы отдельно: ошибки в них сложно отлаживать.
Документируйте метаклассы подробно, так же как и с декораторами — магия внутри них не всегда очевидна.
## Применение метаклассов: система плагинов
Вот компактный, но реальный пример использования метаклассов для создания системы автоматической регистрации плагинов. Без метаклассов этот паттерн реализовывать неудобно: ручная регистрация каждого плагина приводит к дублированию кода и ошибкам, когда разработчик забывает зарегистрировать новый плагин.
```python {.example_for_playground}
class PluginRegistry(type):
plugins = {}
def __new__(cls, name, bases, attrs):
new_cls = super().__new__(cls, name, bases, attrs)
if name != "BasePlugin":
cls.plugins[name] = new_cls
return new_cls
class BasePlugin(metaclass=PluginRegistry):
"""Базовый класс для всех плагинов"""
def execute(self):
raise NotImplementedError()
class EmailPlugin(BasePlugin):
def execute(self):
print("Отправка email уведомления")
class SMSService(BasePlugin):
def execute(self):
print("Отправка SMS сообщения")
def run_plugin(plugin_name):
plugin_class = PluginRegistry.plugins.get(plugin_name)
if not plugin_class:
raise ValueError(f"Плагин '{plugin_name}' не найден")
return plugin_class().execute()
if __name__ == "__main__":
print("Доступные плагины:", list(PluginRegistry.plugins.keys()))
run_plugin("EmailPlugin")
run_plugin("SMSService")
```
```
Доступные плагины: ['EmailPlugin', 'SMSService']
Отправка email уведомления
Отправка SMS сообщения
```
Этот паттерн демонстрирует классический сценарий применения метаклассов — автоматическую регистрацию подклассов. Однако в современном Python ту же задачу решают проще и надёжнее с помощью метода `__init_subclass__`, о котором пойдёт речь далее в этой главе.
## Метаклассы в популярных фреймворках
Примеры в этой главе могут показаться искусственно упрощёнными. Чтобы добиться аналогичного результата, не стоит прибегать к тяжелой артиллерии в виде метаклассов. Сгодятся и декораторы классов. Метаклассы существуют отнюдь не для тривиального изменения поведения классов. Они оказываются действительно незаменимы для решения более сложных задач. Например, когда речь заходит о разработке таких API, как Django ORM, Pydantic, SQLAlchemy.
### Django ORM
[Django](https://www.djangoproject.com/) — это веб-фреймворк. Django ORM (Object-Relational Mapping, объектно-реляционное отображение) — это API, который позволяет взаимодействовать с базой данных, используя код на питоне вместо SQL-запросов. Он описывает записи в SQL-таблицах обычными классами:
```python
class Course(models.Model):
title = models.CharField(max_length=200, unique=True)
chapters_count = models.IntegerField()
course = Course(title="python", chapters_count=37)
course = Course.objects.get(title="python")
print(course)
```
В данном случае мы обратились к полю `chapters_count`, которое в базе данных имеет тип `IntegerField`. Но в коде мы работаем с ним как с обычным `int`, хотя значение `chapters_count` извлекается из таблицы. Это возможно благодаря определенному для `models.Model` метаклассу, который позволяет в стиле питона работать с сущностями из базы данных и избегать сложных запросов.
### Pydantic models
[Pydantic](https://docs.pydantic.dev/latest/) — это библиотека для валидации данных и сериализации. Она автоматически проверяет типы и значения данных при создании объектов. Pydantic защищает код от некорректных данных уже на этапе инициализации объекта. Это особенно полезно при работе со внешними источниками, такими как API, файлы и веб-формы.
Для валидации Pydantic использует аннотации типов в классах. И одним из способов проверки данных являются [модели.](https://pydantic.com.cn/ru/concepts/models/) Это классы, которые наследуются от `pydantic.BaseModel` и определяют поля как аннотированные атрибуты. Получить все зарегистрированные модели можно через вызов `__subclasses__()`:
```python {.example_for_playground}
from pydantic import BaseModel, field_validator
class UserPydantic(BaseModel): # BaseModel - метакласс
name: str
age: int
class CoursePydantic(BaseModel):
id: int
title: str
@field_validator("title", mode="before")
def validate_title(cls, value: str):
return value.upper()
all_models = BaseModel.__subclasses__()
print("Все Pydantic модели:", [cls.__name__ for cls in all_models])
user = UserPydantic(name="John", age=18)
print(f"Модель user: {user}")
print(f"Получения поля модели user: {user.age}")
course = {"id": 1, "title": "python"}
course_model = CoursePydantic(**course) # Парсинг словаря в валидированную модель
print(f"Модель course: {course_model}")
print(f"Тип модели course: {type(course_model)}")
course_dict = course_model.model_dump() # Сериализация модели в словарь
print(f"Данные после сериализации: {course_dict}")
print(f"Тип данных после сериализации: {type(course_dict)}")
```
```
Все Pydantic модели: ['UserPydantic', 'CoursePydantic']
Модель user: name='John' age=18
Получения поля модели user: 18
Модель course: id=1 title='PYTHON'
Тип модели course: <class '__main__.CoursePydantic'>
Данные после сериализации: {'id': 1, 'title': 'PYTHON'}
Тип данных после сериализации: <class 'dict'>
```
Pydantic строго следует аннотациям типов и выбрасывает исключение `ValidationError` (наследуется от `ValueError`), если входные данные не соответствуют ожидаемой модели. Вот два распространённых случая:
```python {.example_for_playground .example_for_playground_001}
# Случай 1: Неверный тип для поля age
try:
user_error = UserPydantic(name="John", age="twenty")
print(user_error)
except ValueError as error:
print("Ошибка валидации:", error)
# Случай 2: Пропущено обязательное поле age
try:
user_error = UserPydantic(name="John")
print(user_error)
except ValueError as error:
print("Ошибка валидации:", error)
```
```
Ошибка валидации: 1 validation error for UserPydantic
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='twenty', input_type=str]
Ошибка валидации: 1 validation error for UserPydantic
age
Field required [type=missing, input_value={'name': 'John'}, input_type=dict]
```
В Pydantic v2 метакласс автоматически собирает поля из аннотаций типов и генерирует валидаторы для типов. Также вручную можно добавить более сложную логику валидирования, как в примере с `field_validator`.
Ознакомиться с тем как реализован метакласс можно [здесь](https://github.com/pydantic/pydantic/blob/f42171c760d43b9522fde513ae6e209790f7fefb/pydantic/_internal/_model_construction.py#L82). А реализацию базового класса можно посмотреть [здесь](https://github.com/pydantic/pydantic/blob/f42171c760d43b9522fde513ae6e209790f7fefb/pydantic/v1/main.py#L316).
### SQLAlchemy declarative
[SQLAlchemy](https://www.sqlalchemy.org/) — мощная библиотека Python для работы с реляционными базами данных. Её ORM позволяет описывать модели данных — то есть Python-классы, каждый из которых соответствует одной таблице в базе данных — и работать с ними как с обычными объектами, не используя SQL напрямую.
Под моделью в контексте SQLAlchemy понимается класс, представляющий структуру таблицы: его атрибуты соответствуют столбцам, а экземпляры — отдельным строкам.
Для автоматической регистрации таких моделей и построения соответствия между классами и таблицами в базе данных SQLAlchemy использует метакласс. Это происходит через вызов `declarative_base()`, который возвращает базовый класс с встроенным метаклассом:
```python {.example_for_playground}
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base() # Конструктор метакласса
# Этот класс нужен для человеко-читаемого представления моделей
class BaseModel(Base):
__abstract__ = True
def __repr__(self):
attrs = []
for column in self.__table__.columns:
value = getattr(self, column.name)
attrs.append(f"{column.name}={value!r}")
return f"<{self.__class__.__name__}({', '.join(attrs)})>"
class UserSQLAlchemy(BaseModel):
__tablename__ = 'UserSQLAlchemy'
id = Column(Integer, primary_key=True)
name = Column(String(50))
class CurseSQLAlchemy(BaseModel):
__tablename__ = 'CurseSQLAlchemy'
id = Column(Integer, primary_key=True)
title = Column(String(100))
print("Все SQLAlchemy таблицы:", list(Base.metadata.tables.keys()))
user = UserSQLAlchemy(id=1, name="John")
curse = CurseSQLAlchemy(id=1, title="Python")
print(user)
print(curse)
```
```
Все SQLAlchemy таблицы: ['UserSQLAlchemy', 'CurseSQLAlchemy']
<UserSQLAlchemy(id=1, name='John')>
<CurseSQLAlchemy(id=1, title='Python')>
```
Ознакомиться с тем, как реализован метакласс можно [здесь](https://github.com/sqlalchemy/sqlalchemy/blob/8383e3f48c900fa248f026218fed0cea5ad0e6a5/lib/sqlalchemy/orm/decl_api.py#L170), реализация конструктора базового класса [здесь](https://github.com/sqlalchemy/sqlalchemy/blob/8383e3f48c900fa248f026218fed0cea5ad0e6a5/lib/sqlalchemy/orm/decl_api.py#L1016).
## Современная альтернатива метаклассам
Начиная с [Python 3.6](https://peps.python.org/pep-0487/) появился более легковесный и читаемый способ кастомизации наследования — метод `__init_subclass__`. Он позволяет переопределить поведение при создании (но только после создания) подклассов без необходимости писать полноценные метаклассы.
```python {.example_for_playground}
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"Creating subclass {cls.__name__} from {cls.__base__.__name__}")
# Можно модифицировать класс здесь
if not hasattr(cls, "default_value"):
cls.default_value = 0
class Child(Base):
pass
class AnotherChild(Base):
default_value = 42
print(Child.default_value)
print(AnotherChild.default_value)
```
```
Creating subclass Child from Base
Creating subclass AnotherChild from Base
0
42
```
Метод `__init_subclass__` — это не полноценная замена метаклассам, а упрощенный инструмент для конкретных задач. На самом деле, `__init_subclass__` работает внутри механизма метаклассов, а не вместо них.
Когда вы определяете `__init_subclass__` в базовом классе, интерпретатор Python автоматически обнаруживает этот метод при создании подкласса, преобразует его в classmethod (даже без явного декоратора `@classmethod`) и вызывает его внутри процесса создания класса — сразу после того, как метакласс (type) завершает построение нового класса.
```python {.example_for_playground}
class Base:
def __init_subclass__(cls, **kwargs):
"""Этот метод автоматически становится classmethod!"""
print(f"Initializing subclass {cls.__name__}")
super().__init_subclass__(**kwargs) # Важно вызывать super(), хотя у нас нет явного родительского класса
class Child(Base):
pass
```
```
Initializing subclass Child
```
Под капотом `__init_subclass__` использует стандартный механизм метаклассов. По сути, это синтаксический сахар для частого использования метаклассов. Вот что происходит при создании класса:
```python {.example_for_playground}
# Когда вы пишете:
class Child(Base):
pass
# Интерпретатор выполняет примерно такой код:
Child = type("Child", (Base,), {})
# Затем автоматически вызывает:
for base in Child.__mro__:
if hasattr(base, "__init_subclass__"):
base.__init_subclass__(Child) # Автоматический вызов
```
Метаклассы дают полный контроль над процессом создания класса:
```python {.example_for_playground}
class Meta(type):
def __new__(cls, name, bases, attrs):
# Полный контроль до создания класса
attrs["added_by_meta"] = "magic"
return super().__new__(cls, name, bases, attrs)
class MyClass(metaclass=Meta):
pass
print(MyClass.added_by_meta)
```
```
magic
```
А `__init_subclass__` дает ограниченный контроль после создания класса:
```python {.example_for_playground}
class Base:
def __init_subclass__(cls, **kwargs):
# Контроль после создания класса
if not hasattr(cls, "default_value"):
cls.default_value = 0
class MyClass(Base):
pass
print(MyClass.default_value)
```
```
0
```
### Когда использовать `__init_subclass__` вместо метаклассов
`__init_subclass__` предпочтительно использовать, когда задача сводится к добавлению или проверке атрибутов у подклассов, требуется базовая валидация при наследовании, важна читаемость кода или нужен доступ к классу сразу после его создания.
Метаклассы применяются, когда требуется полный контроль над процессом создания класса, изменение наследования или пространства имен, перехват создания самого класса, либо необходима сложная логика регистрации и настройки классов.
Перепишем пример с регистрацией плагинов. Откажемся от метаклассов в пользу `__init_subclass__`:
```python {.example_for_playground}
class BasePlugin:
plugins = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if cls.__name__ != "BasePlugin":
BasePlugin.plugins[cls.__name__] = cls
def execute(self):
raise NotImplementedError()
class EmailPlugin(BasePlugin):
def execute(self):
return "Email sent"
class SMSService(BasePlugin):
def execute(self):
return "SMS sent"
print("Registered plugins:", list(BasePlugin.plugins.keys()))
```
```
Registered plugins: ['EmailPlugin', 'SMSService']
```
Код стал проще и понятнее, а логика регистрации оказалась инкапсулирована в базовом классе.
## Проверка наследования: `issubclass()` и метаклассы
Встроенная функция `issubclass()` позволяет проверить, является ли один класс подклассом другого. Это особенно полезно при работе с метаклассами, автоматической регистрацией моделей или динамическим созданием классов — например, чтобы отфильтровать только те классы, которые реализуют определённый интерфейс.
```python {.example_for_playground}
class Serializable:
"""Маркерный базовый класс: объекты его подклассов можно
безопасно сериализовать в JSON."""
def to_dict(self):
raise NotImplementedError
class User(Serializable):
def __init__(self, user_id: int, name: str):
self.user_id = user_id
self.name = name
def to_dict(self):
return {"user_id": self.user_id, "name": self.name}
class DatabaseConnection:
"""Небезопасный для сериализации класс — содержит ресурсы."""
def __init__(self, host: str):
self.host = host
self._connection = None # имитация открытого соединения
def safe_serialize(obj):
"""Сериализует объект, только если его класс унаследован от Serializable.
Защищает от ошибок и утечек при попытке сериализовать неподходящие объекты."""
if issubclass(obj.__class__, Serializable):
return obj.to_dict()
else:
raise TypeError(f"Объект типа {type(obj).__name__} нельзя сериализовать")
# Тестовые данные — могут приходить извне (API, конфиг, плагины и т.д.)
objects = [
User(1, "John"),
DatabaseConnection("localhost"),
]
for obj in objects:
try:
data = safe_serialize(obj)
print(f"Сериализовано: {data}")
except TypeError as error:
print(f"Отклонено: {error}")
```
```
Сериализовано: {'user_id': 1, 'name': 'John'}
Отклонено: Объект типа DatabaseConnection нельзя сериализовать
```
## Сравнение подходов для типичных задач
Кратко перечислим, какие инструменты лучше подходят для решения конкретных задач.
Регистрация классов:
- Метакласс — отлично подходит.
- Декоратор класса — работает, но требует явного применения.
- `__init_subclass__` — отлично подходит и проще в использовании.
Валидация атрибутов:
- Метакласс — полный контроль на этапе создания класса.
- Декоратор класса — валидирует атрибуты, но только после создания.
- `__init_subclass__` — хорошее решение для базовой валидации.
Изменение наследования:
- Метакласс — единственный способ повлиять на цепочку наследования.
- Остальные подходы (декоратор, `__init_subclass__`) — не позволяют этого сделать.
Модификация пространства имён класса:
- Метакласс — полный контроль до создания класса.
- Декоратор класса и `__init_subclass__` — могут модифицировать класс, но только после его создания.
Проверка иерархии классов:
- `issubclass()` / `__subclasses__()` — идеальный инструмент для проверки наследования и получения подклассов.
- Метакласс — может косвенно участвовать (например, при регистрации), но не предназначен для проверки.
## Резюмируем
- Класс — это объект, который создает другие объекты. А метакласс — это объект, который создает классы.
- Метакласс — это фабрика для создания классов в рантайме.
- `type` — это встроенный метакласс, порождающий в питоне все классы.
- От `type` можно наследоваться и таким образом определить кастомный метакласс.
- Для указания, какой метакласс использовать при создании класса, есть ключевое слово `metaclass`.
- Для большинства задач, связанных с наследованием, достаточно современного и читаемого механизма `__init_subclass__`.
- В большинстве прикладных задач метаклассы не нужны. Их основная область применения — создание декларативных API в библиотеках, таких как Django ORM, SQLAlchemy или Pydantic.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!