Сердце Python

Python - это объектно-ориентированный язык программирования. Фактически, часто говорят, что все в Python является объектом.

Итак, чтобы полностью понять Python, вам действительно нужно понимать базовые структуры, такие как объекты и классы, чтобы иметь возможность понять, что на самом деле происходит под капотом.

Эта статья посвящена концепции метапрограммирования в Python, и мы увидим, как управлять классами и создавать их экземпляры.

Метапрограммирование можно условно определить как код, который манипулирует кодом.

Объект, конечно, является экземпляром класса, и, как любопытные существа, которыми мы являемся, когда мы слышим, что в Python все является объектом, в глубине души возникает естественный вопрос (верно?), А именно: классы тогда еще и объекты? и если да, то что их создает?

Давайте разберемся.

Классы и объекты

Первое, что нам нужно обсудить, - это отношения между объектами и классами.

Скажем, у нас есть следующий класс

Мы создаем пустой класс, не содержащий атрибутов и методов. Затем мы создаем его экземпляр в объекте kasper и, наконец, спрашиваем, экземпляр какого класса он создан, с помощью функции type.

Этот код вернет

<class '__main__.Person'>

это означает, что класс, создавший экземпляр объекта kasper, был Person и был определен в том же модуле, который вы запрашивали для типа kasper.

Все в Python - это объект, поэтому мы можем сделать то же самое для встроенных типов, таких как

который распечатает

<class 'str'>

Таким образом, строка «Medium» - это объект, созданный классом str.

Метаклассы

Хорошо, но что тогда за класс? Что ж, давай попробуем.

На выходе

<class 'type'>
<class 'type'>
<class 'type'>

Интересно. Похоже, что тип класса - тип.

Это потому, что тип - это так называемый метакласс. Метакласс создает экземпляры классов так же, как классы создают экземпляры объектов.

Это все хорошо, но как это использовать?

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

Эта концепция изменения поведения путем манипулирования кодом перед выполнением хорошо известна по декораторам.

Декораторы в Python позволяют нам манипулировать функциями перед их вызовом, и на самом деле, как мы вскоре увидим, у них много общего.

Создание экземпляров класса

Когда мы говорим о метаклассах, первое, что нужно понять, - это то, как класс в Python создается с помощью метакласса по умолчанию type.

При создании экземпляра класса Python захватывает некоторые переменные, такие как имя класса и базовые классы, от которых наследуется класс, и в основном формирует словарь, вызывая метод dunder для типа с именем __prepare__.

Затем выполняется тело класса, чтобы заполнить этот словарь атрибутами и методами.

Последний шаг - дать имя, базовые классы и указанный выше словарь в качестве параметров для класса type. Это выглядит так:

Person = type('Person', (Base1, Base2), cls_dict)

Объект Person теперь является классом, который наследуется от своих родителей Base1 и Base2 и имеет атрибуты и методы, указанные в cls_dict.

Если бы мы воссоздали пустой класс с именем Person, мы могли бы сделать это динамически следующим образом

Person = type('Person', (), {})

Это эквивалентно приведенному выше определению, где мы использовали ключевое слово pass для создания пустого класса.

Как мы создаем собственные метаклассы?

Рассмотрим следующий пример:

Здесь мы создаем собственный метакласс с именем custom, в котором мы перезаписываем dunder-метод type __new__.

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

Метод __new__ возвращает класс при вызове, и мы получаем возможность манипулировать параметрами класса перед его созданием.

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

Учти это:

Это вернет

WOOOF!

Без проблем. Но если мы попробуем запустить следующее:

он выйдет из строя со следующим сообщением

TypeError: More than two methods

Метаклассы и декораторы

Допустим, у нас есть группа классов, которые наследуются от одного и того же базового класса, и мы хотим выполнить автоматическую отладку всех методов всех этих классов.

Мы также хотим избежать дублирования кода, следуя принципу DRY (не повторяйтесь). Как лучше всего решить эту проблему?

Очевидно, что на ум приходят декораторы, и действительно, концепция декораторов, в которых мы можем делать все, что угодно, до и после выполнения функции (или класса), - это хороший инструмент для отладки, который есть в вашем распоряжении, и массивный оптимизатор DRY.

Давайте создадим настраиваемый отладчик, который может сказать нам, сколько параметров передается методам в классе.

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

Вывод этого кода:

arguments has 3 arguments
16

Довольно мило. Оператор печати в функции-оболочке выполняется до вызова функции arguments.

Обратите внимание, что приведенный выше синтаксис @ - это просто синтаксический сахар. Что действительно происходит за кулисами, так это то, что вы передаете функцию в качестве параметра декоратору при вызове функции.

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

Рассмотрим следующий декоратор пользовательского класса:

Теперь мы можем определить класс с этим декоратором наверху, чтобы отладить все наши методы (кроме методов класса и статических методов) за один проход.

На выходе

walk has 1 arguments
miaaw has 1 arguments

Все это замечательно, но вспомните из приведенной выше постановки задачи, что у нас есть много классов, наследующих от одного и того же базового класса.

Можем ли мы как-то использовать это наследование, чтобы сохранить все те строки кода, где нам нужно разместить декораторы?

Ну да. Мы могли бы использовать собственный метакласс.

Допустим, общий базовый класс, от которого наследуются все наши классы, называется Base. Затем мы могли бы создать следующий метакласс, который затем мог бы наследовать Base.

Все наши классы, унаследованные от Base, теперь будут иметь все методы, отлаженные с помощью декоратора debug_function.

Когда мне следует использовать метакласс?

Метаклассы редко используются на практике, потому что есть много других решений большинства проблем, где можно было бы использовать метаклассы. Например, декораторы дают нам общий мощный инструмент, которого в большинстве случаев бывает достаточно.

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

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

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

Декораторы не дают вам этого, потому что на самом деле экземпляр класса создается до того, как вы сделаете что-либо с декоратором.

Надеюсь, эта статья оказалась для вас полезной.

Как всегда, если у вас есть какие-либо вопросы, комментарии или опасения, не стесняйтесь обращаться к LinkedIn.