# Глава 18. Модель данных
Обсудим объектную модель в питоне. Разберемся, что такое классы и инстанцируемые от них объекты с точки зрения интерпретатора. А также для чего нужны магические методы и почему они так называются.
## Как соотносятся классы и объекты
Все в питоне является объектом: число, кортеж, функция... Даже класс. Класс — это объект с типом `type`. Термины «класс» и «тип» здесь взаимозаменяемы.
Воспользуемся функцией `type()`, которая принимает объект и возвращает его тип:
```python {.example_for_playground}
print(type(int))
print(type(list))
```
```
<class 'type'>
<class 'type'>
```
Так как класс — это объект, то классы `int` и `list` без проблем были переданы в функцию и для них вернулся тип `type`.
Выведите в консоль, какой класс у `type`. {.task_text}
```python {.task_source #python_chapter_0180_task_0010}
```
Нужно вызвать встроенную функцию `type()`, передав в нее аргумент `type`. {.task_hint}
```python {.task_answer}
print(type(type))
```
## Dunder-атрибуты (магические атрибуты) {#block-dunder-attributes}
В прошлых главах мы [намекнули](/courses/python/chapters/python_chapter_0160#block-dunder-attributes) на существование специальных полей и методов класса с зарезервированными именами.
Например, есть метод `__init__()`, который неявно отрабатывает в момент инициализации объекта. Или метод `__len__()`, после имплементации которого к классу становится возможным применить встроенную функцию `len()`. В игру вступает утиная типизация: встроенные функции полагаются на наличие атрибутов с определенными именами.
Это так называемые **dunder-атрибуты** (от словосочетания «double underscore», двойное подчеркивание в имени). Они также известны как **магические атрибуты,** потому что привносят в поведение объектов некую магию. Напрямую почти никогда не вызываются, зато добавляют в класс новый функционал, например:
- Вызов объекта через оператор `()`: `__call__()` превращает инстанс класса в функциональный объект.
- Сравнение через такие операторы как `<`, `==`, `>=`. Для них предусмотрен набор методов: `__lt__()`, `__eq__()`, `__ge__()`, ...
- Сложение через `+`, возведение в степень, взятие по модулю и другие математические операции, выраженные такими методами как `__add__()`, `__sub__()`.
- Работа с объектом как с коллекцией. Методы `__getitem__(self)`, `__setitem__()`, `__delitem__()` реализуют обращение к элементу контейнера по ключу через оператор `[]`, инициализацию и удаление элемента.
Остановимся подробнее на dunder-методах `__str__()` и `__repr__()`, отвечающих за строковое представление экземпляра класса. Если в классе не определен ни один из них, то мы получаем стандартную и малоинформативную строку об объекте: {#block-str-repr}
```python {.example_for_playground}
class Example:
def __init__(self):
self.a = 10
self.b = 5
print(Example())
```
```
<__main__.Example object at 0x7f67b1a8f590>
```
Но стоит добавить в класс метод `__str__()`, и консольный вывод изменится.
```python {.example_for_playground}
class Example:
def __init__(self):
self.a = 10
self.b = 5
def __str__(self):
return f"Example: a = {self.a}, b = {self.b}"
print(Example())
```
```
Example: a = 10, b = 5
```
Метод `__repr__()` тоже позволяет выводить об объекте информацию в виде строки. Этим он схож с `__str__()`, однако между ними есть важные различия.
`__str__()` отвечает за читабельное представление объекта для пользователя; `__repr__()` используется разработчиками в отладочных целях и должен ясно идентифицировать объект. Удобно, если `__repr__()` выводит строку, в которой прописан инициализатор объекта с переданными в него аргументами. Чтобы разработчик мог скопировать эту строку и воссоздать объект с такими же характеристиками.
`__str__()` вызывается в функциях `str()` и `print()`, при форматировании через `format()` или f-строку. `__repr__()` вызывается в функции `repr()` и при выводе объекта на экран в интерактивном интерпретаторе.
Если в классе определен `__repr__()`, но не определен `__str__()`, то `__repr__()` вызывается вместо него в перечисленных выше ситуациях.
Руководствуясь описанными выше рекомендациями, имплементируйте в классе `Storage` dunder-методы `__str__()` и `__repr__()`. {.task_text}
```python {.task_source #python_chapter_0180_task_0020}
class Storage():
def __init__(self, message_size_limit):
self.message_size_limit = message_size_limit
```
Метод `__repr__()` должен возвращать строку-инициализатор. {.task_hint}
```python {.task_answer}
class Storage():
def __init__(self, message_size_limit):
self.message_size_limit = message_size_limit
def __str__(self):
return f"Storage with message_size_limit {self.message_size_limit}"
def __repr__(self):
return f"Storage({self.message_size_limit})"
```
## Базовый класс object
Все классы в современном питоне (начиная с 3-й версии языка) неявно наследуются от класса `object`. Благодаря этому они перенимают от `object` набор dunder-атрибутов.
Поле `__bases__` как раз один из них. В нем хранится перечисление родительских классов. Их может быть больше одного, так как в языке поддерживается множественное наследование.
```python {.example_for_playground}
print(f"base of bool: {bool.__bases__}")
print(f"base of int: {int.__bases__}")
print(f"base of object: {object.__bases__}")
```
```
base of bool: (<class 'int'>,)
base of int: (<class 'object'>,)
base of object: ()
```
Родителем класса `bool` является `int`. Родителем `int` — класс `object`. А `object` в свою очередь ни от какого класса не наследуется.
Выведите в консоль результат проверки, является ли класс `list` производным классом `object`. {.task_text}
Для этого используйте встроенную функцию `issubclass()`, принимающую 2 аргумента: производный и базовый классы. {.task_text}
```python {.task_source #python_chapter_0180_task_0030}
```
Требуется вызвать функцию `issubclass()` от двух аргументов: `list` и `object`. {.task_hint}
```python {.task_answer}
print(issubclass(list, object))
```
Заведите список с именем `lst`. Выведите в консоль результат проверки, является ли объект `lst` экземпляром класса `object`. {.task_text}
Для этого воспользуйтесь встроенной функцией `isinstance()`, принимающей 2 аргумента: объект и класс. {.task_text}
```python {.task_source #python_chapter_0180_task_0040}
```
Требуется вызвать функцию `isinstance()` от двух аргументов: списка и `object`. {.task_hint}
```python {.task_answer}
lst = []
print(isinstance(lst, object))
```
Так как `type` — это класс, а все классы наследуются от `object`... то `type` тоже наследуется от `object`. Кого-то данный факт шокирует.
Выведите в консоль на разных строках: {.task_text}
- Перечисление родительских классов для `type`.
- Флаг, является ли `type` дочерним классом `object`.
```python {.task_source #python_chapter_0180_task_0050}
```
Для получения родительских классов воспользуйтесь полем `__bases__` класса `type`. Для определения, является ли `type` потомков `object`, вызовите функцию `issubclass()` от `type` и `object`. {.task_hint}
```python {.task_answer}
print(type.__bases__)
print(issubclass(type, object))
```
Вот мы и выяснили, как в питоне выглядит [интроспекция,](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D1%80%D0%BE%D1%81%D0%BF%D0%B5%D0%BA%D1%86%D0%B8%D1%8F_(%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)) то есть получение для объекта в рантайме типа, списка методов и полей, базовых классов и других свойств. Для интроспекции предназначены некоторые dunder-атрибуты и использующие их под капотом встроенные функции, такие как `isinstance()` и `issubclass()`.
## Объектная модель
У каждого объекта есть id, тип, значение и счетчик ссылок для сборщика мусора. Id и тип неизменны на протяжении всей жизни объекта. Зато значения объектов некоторых типов могут меняться: как мы помним, типы делятся на изменяемые и неизменяемые. К неизменяемым относятся только числа, флаги, строки и кортежи. Все остальные классы, включая пользовательские, являются изменяемыми.
Любая переменная в питоне — это имя для [ссылки](https://ru.wikipedia.org/wiki/%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%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)) на объект в памяти. Связывание имени и ссылки на объект происходит в момент присваивания, например при вызове оператора `=`.
С помощью ключевого слова `class` создадим пустой пользовательский класс `Dummy`. Инстанцируем экземпляр `x` этого класса, выведем его id и тип.
```python {.example_for_playground}
class Dummy:
pass
x = Dummy()
print(f"id: {id(x)}")
print(f"type: {type(x)}")
```
```
id: 140225567618320
type: <class '__main__.Dummy'>
```
Откуда интерпретатор получил id объекта? id — это уникальное число, которое CPython выводит на основе адреса объекта в памяти. Поэтому если две переменные в коде ссылаются на один и тот же объект, их id совпадают.
Каким образом интерпретатор установил тип объекта, возвращенный функцией `type()`? Здесь нам пригодится знание, что все классы наследуются от `object`. От него они получают dunder-атрибуты, среди которых `__class__` — поле, содержащее тип.
## Внутреннее представление атрибутов объекта {#block-dict}
Понимание dunder-атрибутов приоткрывает завесу тайны над внутренним устройством объектов: на самом деле атрибуты объектов хранятся в специальном словаре `__dict__`. Да, это всего лишь пары ключ-значение!
```python {.example_for_playground}
class Example:
_id_cls = 0
def __init__(self):
self._id_obj = 0
print("Class attributes:", Example.__dict__)
print("\nInstance attributes:", Example().__dict__)
```
```
Class attributes: {'__module__': '__main__', '_id_cls': 0, '__init__': <function Example.__init__ at 0x7f47e4213240>, '__dict__': <attribute '__dict__' of 'Example' objects>, '__weakref__': <attribute '__weakref__' of 'Example' objects>, '__doc__': None}
Instance attributes: {'_id_obj': 0}
```
Из примера видно, что в словаре класса хранится `_id_cls` — поле класса. А в словаре инстанса хранится `_id_obj` — поле объекта.
Невероятно! Неужели для каждого объекта в питоне выделяется память под ключи и значения словаря `__dict__`? Насколько же это не эффективно! К счастью, начиная с версии 3.3 языка хранение `__dict__` оптимизировано. Теперь словари, использующиеся для хранения атрибутов, разделяют одни и те же ключи между разными инстансами классов. С подробностями можно ознакомиться в [PEP-412.](https://peps.python.org/pep-0412/)
Выведите в консоль содержимое атрибута `__dict__` класса `list`, по строке на каждую пару ключ-значение. Формат строки: `{ключ} {значение}`. {.task_text}
Посмотрите, какие dunder-атрибуты и пользовательские атрибуты есть у списка. Под **пользовательскими** имеются ввиду обычные поля и методы с не зарезервированными именами. {.task_text}
```python {.task_source #python_chapter_0180_task_0060}
```
Требуется проитерироваься по `list.__dict__.items()`. {.task_hint}
```python {.task_answer}
for k, v in list.__dict__.items():
print(k, v)
```
Схожую с полем `__dict__` роль выполняет метод `__dir__()`: его можно самостоятельно определить в классе, чтобы он возвращал список атрибутов. Это бывает полезно, если в классе реализована [дополнительная логика](https://docs.python.org/3/reference/datamodel.html#object.__getattr__) для доступа к существующим и не существующим атрибутам объекта.
Полезно знать и про встроенную функцию `dir()`. Вызванная без параметров, она возвращает список имен в локальной области видимости. Если же в нее передать объект, `dir()` возвращает вывод `__dir__()` (при условии, что этот метод определен), либо отсортированные ключи `__dict__` с добавлением унаследованных от родительского класса атрибутов. Конкретная реализация `dir()` [зависит](https://docs.python.org/3/library/functions.html#dir) от версии языка. {#block-dir}
## Monkey patching
Раз атрибуты объекта — это всего лишь записи в поле `__dict__`, то неудивительно, что интерпретатор позволяет динамически добавлять объектам новые атрибуты вне зависимости от инстанцируемого класса. Такой прием называется «monkey patching».
Заведите пустой класс `DataRow`. Создайте экземпляр этого класса с именем `dr`. {.task_text}
Присвойте полю `field` объекта `dr` значение `"VALUE"`. {.task_text}
Выведите в консоль в формате `"{ключ} {значение}"` элементы, появившиеся в поле `__dict__` объекта `dr` **после присваивания.** {.task_text}
```python {.task_source #python_chapter_0180_task_0070}
```
Чтобы определить, какие элементты появились в `__dict__` после присваивани, нужно предварительно сохранить в объект-множество ключи `__dict__` до присваивания и сравнить их с ключами после. Например, через разность множеств. {.task_hint}
```python {.task_answer}
class DataRow:
...
dr = DataRow()
keys_before = set(dr.__dict__)
dr.field = "VALUE"
for k in (set(dr.__dict__) - keys_before):
print(k, dr.__dict__[k])
```
Теперь понятно, что набор атрибутов разных объектов одного и того же класса может отличаться.
Monkey patching, то есть обновление поведения кода в рантайме, используется для изменения поведения классов, модулей, функций без затрагивания напрямую их кода. Этим приемом не рекомендуется злоупотреблять. Зато он отлично демонстрирует одно из проявлений в языке [рефлексии](https://ru.wikipedia.org/wiki/%D0%9E%D1%82%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5_(%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)) — способности исполняемого кода изменять собственную структуру и поведение.
## Резюмируем
- Все в питоне — это объект (даже класс).
- Классы — это объекты, которые имеют тип `type`.
- Все классы неявно наследуются от базового класса `object`.
- Dunder-атрибуты (магические атрибуты) позволяют операторам и встроенным функциям полиморфно работать с классами.
- Атрибуты объектов хранятся в dunder-атрибуте `__dict__`, то есть являются записями в словаре.
- Для интроспекции используются такие встроенные функции как `id()`, `type()`, `isinstance()`, `issubclass()` и другие.
- Monkey patching — переопределение или добавление атрибутов в рантайме. Это один из вариантов рефлексии.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!