Новости
18.05.2021
Книга «Python. Лучшие практики и инструменты»
Третье издание «Python. Лучшие практики и инструменты» даст вам инструменты для эффективного решения любой задачи разработки и сопровождения софта. Авторы начинают с рассказа о новых возможностях Python 3.7 и продвинутых аспектах синтаксиса Python. Продолжают советами по реализации популярных парадигм, в том числе объектно-ориентированного, функционального и событийно-ориентированного программирования. Также авторы рассказывают о наилучших практиках именования, о том, какими способами можно автоматизировать развертывание программ на удаленных серверах. Вы узнаете, как создавать полезные расширения для Python на C, C++, Cython и CFFI.
Паттерны доступа к расширенным атрибутам
Изучая Python, многие программисты C++ и Java удивляются отсутствию ключевого слова private. Наиболее близкая к нему концепция — это искажение (декорирование) имени (name mangling). Каждый раз, когда атрибут получает префикс __, он динамически переименовывается интерпретатором:
Доступ к атрибуту __secret_value по его изначальному имени приведет к выбрасыванию исключения AttributeError:
Это сделано специально для того, чтобы избежать конфликта имен по наследованию, так как атрибут переименовывается именем класса в качестве префикса. Это не точный аналог private, поскольку атрибут может быть доступен через составленное имя. Данное свойство можно применить для защиты доступа некоторых атрибутов, однако на практике __ не используется никогда. Если атрибут не является публичным, то принято использовать префикс _. Он не вызывает алгоритм декорирования имени, но документирует атрибут как приватный элемент класса и является преобладающим стилем.
В Python есть и другие механизмы, позволяющие отделить публичную часть класса от приватной. Дескрипторы и свойства дают возможность аккуратно оформить такое разделение.
Дескрипторы
Дескриптор позволяет настроить действие, которое происходит, когда вы ссылаетесь на атрибут объекта.
Дескрипторы лежат в основе организации сложного доступа к атрибутам в Python. Они используются для реализации свойств, методов, методов класса, статических методов и надтипов. Это классы, которые определяют, каким образом будет получен доступ к атрибутам другого класса. Иными словами, класс может делегировать управление атрибута другому классу.
Классы дескрипторов основаны на трех специальных методах, которые формируют протокол дескриптора:
__set__(self, obj, value) — вызывается всякий раз, когда задается атрибут. В следующих примерах мы будем называть его «сеттер»;
__get__(self, obj, owner=None) — вызывается всякий раз, когда считывается атрибут (далее геттер);
__delete__(self, object) — вызывается, когда del вызывается атрибутом.
Дескриптор, который реализует __get__ и __set__, называется дескриптором данных. Если он просто реализует __get__, то называется дескриптором без данных.
Методы этого протокола фактически вызываются методом __getattribute__() (не путать с __getattr__(), который имеет другое назначение) при каждом поиске атрибута. Всякий раз, когда такой поиск выполняется с помощью точки или прямого вызова функции, неявно вызывается метод __getattribute__(), который ищет атрибут в следующем порядке.
- Проверяет, является ли атрибут дескриптором данных на объекте класса экземпляра.
- Если нет, то смотрит, найдется ли атрибут в __dict__ объекта экземпляра.
- Наконец, проверяет, является ли атрибут дескриптором без данных на объекте класса экземпляра.
Вот пример его использования в интерактивном режиме:
Иными словами, дескрипторы данных имеют приоритет над __dict__, который, в
Вот пример его использования в интерактивном режиме:свою очередь, имеет приоритет над дескрипторами без данных.
Для ясности приведем пример из официальной документации Python, в котором показано, как дескрипторы работают в реальном коде:
Вот пример его использования в интерактивном режиме:
Пример ясно показывает, что если класс имеет дескриптор данных для этого атрибута, то вызывается метод __get__(), чтобы вернуть значение каждый раз, когда извлекается атрибут экземпляра, а __set__() вызывается всякий раз, когда такому атрибуту присваивается значение. Использование метода __del__ в предыдущем примере не показано, но должно быть очевидно: он вызывается всякий раз, когда атрибут экземпляра удаляется с помощью оператора del instance.attribute или delattr(instance, 'attribute').
Разница между дескрипторами с данными и без имеет большое значение по причинам, которые мы упомянули в начале подраздела. В Python используется протокол дескриптора для связывания функций класса с экземплярами через методы. Они также применяются в декораторах classmethod и staticmethod. Это происходит потому, что функциональные объекты по сути также являются дескрипторами без данных:
Это верно и для функций, созданных с помощью лямбда-выражений:
Таким образом, если __dict__ не будет иметь приоритет над дескрипторами без данных, мы не сможем динамически переопределить конкретные методы уже созданных экземпляров во время выполнения. К счастью, благодаря тому, как дескрипторы работают в Python, это возможно; поэтому разработчики могут выбирать, в каких экземплярах что работает, не используя подклассы.
Пример из реальной жизни: ленивое вычисление атрибутов. Один из примеров использования дескрипторов — задержка инициализации атрибута класса в момент доступа к нему из экземпляра. Это может быть полезно, если инициализация таких атрибутов зависит от глобального контекста приложения. Другой случай — когда такая инициализация слишком затратна, и неизвестно, будет ли атрибут вообще использоваться после импорта класса. Такой дескриптор можно реализовать следующим образом:
Ниже представлен пример использования:
Официальная библиотека OpenGL Python на PyPI под названием PyOpenGL использует такую технику, чтобы реализовать объект lazy_property, который является одновременно декоратором и дескриптором данных:
Такая реализация аналогична использованию декоратора property (о нем поговорим позже), но функция, которая оборачивается декоратором, выполняется только один раз, а затем атрибут класса заменяется значением, возвращенным этим свойством функции. Данный метод часто бывает полезен, когда необходимо одновременно выполнить два требования:
- экземпляр объекта должен быть сохранен как атрибут класса, который распределяется между его экземплярами (для экономии ресурсов);
- этот объект не может быть инициализирован в момент импорта, поскольку процесс его создания зависит от некоего глобального состояния приложения/контекста.
В случае приложений, написанных с использованием OpenGL, вы будете часто сталкиваться с такой ситуацией. Например, создание шейдеров в OpenGL обходится дорого, поскольку требует компиляции кода, написанного на OpenGL Shading Language (GLSL). Разумно создавать их только один раз и в то же время держать их описание в непосредственной близости от классов, которым они нужны. С другой стороны, шейдерные компиляции не могут быть выполнены без инициализации контекста OpenGL, так что их трудно определить и собрать в глобальном пространстве имен модуля на момент импорта.
С полным содержанием статьи можно ознакомиться на сайте "Хабрахабр":
Комментарии: 0
Пока нет комментариев