# Глава 37. Метаклассы
> Метаклассы — потаенная магия питона, о которой 99% разработчиков не должны даже задумываться.
Тим Питерс, core-контрибьютор питона и автор сортировки Timsort
Как сказал Тим Питерс, если вы задумываетесь, нужно ли вам использовать метаклассы, то нет, не нужно. И это правило железно работает... За исключением того, что о метаклассах нет-нет да спрашивают на собеседованиях. А в дебрях сложных проектов, с которыми приходится сталкиваться, проскальзывает таинственное слово `metaclass`.
Метакласс — мощный инструмент метапрограммирования. Метакласс — это фабрика классов. С ее помощью классы создаются и кастомизируются прямо в рантайме. Метаклассами пронизана подкапотная работа с классами. Если хотите знать больше, то эта глава для вас.
## Что такое метакласс
Как мы [выяснили](/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"))
```
Метаклассы выполняют довольно простую работу: перехватывают создание класса; модифицируют класс; возвращают уже модифицированный. Вот собственно и все, что нужно знать о метаклассах.
## Применение метаклассов
И примеры, и задачи в этой главе выглядят притянутыми за уши. Чтобы добиться аналогичного результата, не стоит прибегать к тяжелой артиллерии в виде метаклассов. Сгодятся и декораторы классов. Метаклассы существуют отнюдь не для тривиального изменения поведения классов. Они оказываются действительно незаменимы для решения более сложных задач. Например, когда речь заходит о разработке API, таких как Django ORM.
Django — это веб-фреймворк. 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)
print(course.chapters_count)
```
В данном случае мы обратились к полю `chapters_count`, которое в бд имеет тип `IntegerField`. Но в коде мы работаем с ним как с обычным `int`, хотя значение `chapters_count` извлекается из таблицы. Это возможно благодаря определенному для `models.Model` метаклассу, который позволяет в стиле питона работать с сущностями из бд и избегать сложных запросов.
## Резюмируем
- Класс — это объект, который создает другие объекты. А метакласс — это объект, который создает классы.
- Метакласс — это фабрика для создания классов в рантайме.
- `type` — это встроенный метакласс, порождающий в питоне все классы.
- От `type` можно наследоваться и таким образом определить кастомный метакласс.
- Для указания, какой метакласс использовать при создании класса, есть ключевое слово `metaclass`.
- Чаще всего метаклассы используются для реализации API, таких как Django ORM.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!