Перейти к содержанию

Магические методы и протоколы

Магические методы в Python

Python язык, который имеет свои особенности. В этой стать мы разберемся с магическими методами. На английском языке они называются magic methods или dunder methods (от double under), потому что начинаются с двух символов подчеркивания, например __init__.

Что же такое магические методы? Они играют очень важную роль в дизайне языка. Если очень кратко, то с помощью магических методов можно добавлять магию классам добавляя специальные методы, которые начинаются и заканчиваются с двух знаков подчеркивания.

К сожалению, эти методы не очень хорошо объяснены в официальной документации, да и в блогах информация обрывочная и не дает полной картины. Но при этом понимание этой информации является ключом к пониманию внутренних процессов языка. То есть теперь вы получите суперспособность видеть еще один важный слой устройства языка Python.

В этой лекции мы будем системно проходить по разным магическим методам и знакомиться с их применением.

Конструкторы и инициализация объектов

В прошлых лекциях и при просмотре чужого кода вы уже сталкивались с методом __init__. Этот самый распространенный магический метод, его еще называют конструктором. Потому что он конструирует объект. С его помощью него можно управлять процессом создания объекта. Когда исполняется код x = SomeClass(...) то обязательно исполняется метод __init__, даже если он пустой и ничего не делает.

На самом деле он вызывается не первым, но с его сестринским методом __new__ мы глубоко разбираться не будем.

Для базового понимания процесса формирования объектов в Python давайте разберем этот процесс.

Представим, себе такой класс:

class SomeClass():

    prop = None

    def __init__(self, arg, kwarg=None):
        pass

    def some_method(self):
        ...

Что же происходит, когда в коде исполняется строка x = SomeClass(...)?

Сначала создается основа объекта, это что-то типа словаря, в котором перечислены все методы и свойства объекта. И потом в этот объект передается набор аргументов, которые были перечислены в скобках и выполняется код конструктора класса __init__. И его код исполняется уже в контексте объекта и к экземпляру самого объекта можно обращаться через специальную переменную self.

Копнуть глубже

На самом деле до __init__ вызывается метод __new__. С его помощью можно управлять процессом создания объекта или фильтровать входящие аргументы.

Если вам очень хочется узнать о работе __new__, то более подробно можно прочитать в документации по версии Python в которой был добавлен этот механизм.

И если хочется копнуть еще глубже, то скажу, что можно управлять и процессом создания класса, объект которого будет создаваться. Эта техника называется мета программирование. В этот момент можно управлять какие методы будут у класса и как они будут себя вести. Например, для каждого свойства сделать еще дополнительные свойства, которые будут преобразовывать значения каким-то образом. Это тема для более глубокого изучения.

Если есть конструктор, с помощью которого можно управлять созданием объекта. То должен быть и метод с помощью которого можно управлять удалением объекта. Такой метод есть, и он называется деструктор, а его имя __del__. Оба метода является частью протокола конструктора и управляют созданием и удалением объекта. Он срабатывает в тот момент, когда сборщик мусора уничтожает объект из памяти. Но не работает в тот момент, когда исполняется код del x. К сожалению, исполнение метода __del__ не гарантируется. Например, если интерпретатор завершается аварийно.

Обычно этот метод реализуется, когда происходит задействование каких-то внешних ресурсов, например при создании объекта открывается файл или сетевое соединение. А после удаления объекта их хочется закрыть.

Пример кода с использованием обоих методов:

class FileObj:

    def __init__(self, fname=None):
        self.file = open(fname, 'r+')

    def __del__(self):
        self.file.close()
        def self.file

Какая идея находится за магическими методами

После того как мы обсудили первые магические методы давайте разберемся с общей идеей. Python — это язык с уникальным дизайном в основе которого лежат объекты и их взаимодействие. Взаимодействие тут ключевое слово. С одной стороны объект — это данные. С другой это методы, которые позволяют эти данные обрабатывать, сохранять, читать или пересылать, то есть взаимодействовать. В процессе написания программы мы создаем множество разных типов объектов, но часто у них есть очень схожие характеристики. Например, есть объекты, которые, можно вывести на экран, или множества (списки), по которым хочется проходить одинаковым способом. И возникает вопрос стандартизации похожих видов взаимодействий для похожих объектов. Объекты разные, а взаимодействия типичные.

Для формирования похожей типологии общения с объектами в науке информатике используется несколько схожих механизмов. В компьютерной литературе встречаются специальные термины, которые объясняют как эти можно реализовать так чтобы разные объекты вели себя однотипно: реализация интерфейса, утиная типизация, наследование, реализация протокола. В Python для систематизации работы разных, но похожих объектов используются протоколы. И их в языке целая россыпь.

Добавление магического метода с нужной сигнатурой позволяет сделать объект доступным для какого-то протокола. Например, сигнатура метода __init__ определяет какая будет сигнатура конструктора объекта (какие параметры надо передать классу, чтобы создать его экземпляр).

Для того чтобы объект поддерживал срезы или мог работать с операторами надо добавить свои магические методы. Более сложные протоколы требуют реализации и методов и их особое поведение. Например: протокол итератора позволяет сделать, так чтобы объект мог использоваться источником последовательности для цикла for. Для этого надо реализовать методы __iter__ и __next__ (__aiter__ и __anext__ для асинхронного итератора), а когда данные закончится надо вместо ответа метода __next__ вызывать специальное исключение StopIterationStopAsyncIteration).

Поэтому Python называют языком протоколов.

Работа с определениями

В целом мне кажется, что утиная типизация является частным случаем работы протоколов. В Википедии написано, что для того, чтобы объекты вели себя похожим образом, то надо реализовать у них похожие методы. А термин "протокол" расширяет это поведение и помимо методов надо еще и реализовать другие формы поведения объекта.