Главная / Курсы / Python / Модель данных
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!