Что такое дескрипторы: структура, примеры

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

Определение дескрипторов

Дескриптор в Python - это любой объект, у которого определены специальные методы __get__(), __set__() или __delete__(). Эти методы изменяют поведение атрибутов класса, к которым привязан дескриптор.

Когда мы обращаемся к атрибуту класса, содержащему дескриптор, вызываются магические методы этого дескриптора. Например, при получении значения атрибута вызывается метод __get__(). А при присваивании значения - метод __set__().

парень за компьютером

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

Типы дескрипторов

Различают 2 основных типа дескрипторов:

  • Дескрипторы данных (data descriptors) - реализуют методы __set__() и __delete__()
  • Дескрипторы не-данных (non-data descriptors) - реализуют только метод __get__()

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

Протокол дескрипторов

Рассмотрим подробнее методы, которые могут определять дескрипторы:

  • __get__(self, obj, type) - для получения значения атрибута
  • __set__(self, obj, value) - для установки значения атрибута
  • __delete__(self, obj) - для удаления атрибута

Параметры методов:

  • self - ссылка на экземпляр дескриптора
  • obj - экземпляр класса, содержащего дескриптор
  • value - значение, которое присваивается атрибуту
  • type - класс, к которому принадлежит дескриптор

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

Пример простого дескриптора

Например, реализуем простой дескриптор данных для валидации атрибута:

 class Validated: def __init__(self, name): self.name = name def __get__(self, obj, objtype): return getattr(obj, self.name) def __set__(self, obj, value): if not isinstance(value, int): raise TypeError('Expected int') if value < 0: raise ValueError('Must be positive') setattr(obj, self.name, value) class Person: age = Validated('age') 

Теперь при попытке установить отрицательный возраст будет выброшено исключение ValueError. А при присваивании значения неверного типа - TypeError.

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

Где используются дескрипторы

Дескрипторы широко применяются в самом Python. Например:

  • Функции - это дескрипторы не-данных
  • Методы класса реализованы через дескрипторы
  • Декораторы @property, @staticmethod, @classmethod - это дескрипторы
компьютер стоит на столе

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

  • Валидации данных
  • Логирования доступа к атрибутам
  • Кэширования результатов методов
  • Отложенной инициализации атрибутов

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

Структура дескриптора

По сути дескриптор - это обычный класс в Python, реализующий специальные методы __get__(), __set__() или __delete__().

Рассмотрим структуру дескриптора на примере:

 class Typed: def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, obj, cls): value = getattr(obj, self.name) # код проверки типа return value def __set__(self, obj, value): if not isinstance(value, self.expected_type): raise TypeError('Must be a %s' % self.expected_type) setattr(obj, self.name, value) def __delete__(self, obj): # код удаления 

Это пример дескриптора, который позволяет проверять тип атрибута. Используем его так:

 class Person: name = Typed('name', str) p = Person() p.name = 'Bob' # Ok p.name = 123 # Error! 

Видно, что при помощи дескриптора мы контролируем тип атрибута name.

Примеры дескрипторов

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

  • Логирование обращений - дескриптор, который записывает в лог каждое обращение к атрибуту.

  • Кэширование - дескриптор, который сохраняет результат метода в кэше и возвращает кэшированное значение при повторных вызовах.

  • Отложенная инициализация - дескриптор, который инициализирует атрибут только при первом обращении к нему.

  • Ограничение доступа на запись - дескриптор, который делает атрибут доступным только для чтения.

Это лишь некоторые примеры полезной функциональности, которую можно реализовать при помощи дескрипторов в Python.

Когда применять дескрипторы

Использование дескрипторов дает ряд преимуществ:

  • Гибкость и расширяемость кода
  • Повторное использование функциональности между классами
  • Автоматизация рутинных операций с атрибутами
  • Инкапсуляция и сокрытие реализации

Однако есть и недостатки:

  • Более сложный и неочевидный код
  • Нестандартное поведение атрибутов

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

Часто задаваемые вопросы

Рассмотрим ответы на частые вопросы про дескрипторы в Python:

  1. Как отличить дескриптор от обычного класса?

    Copy code

    По наличию методов __get__(), __set__() или __delete__().

  2. Можно ли сделать дескриптор из функции?

    Copy code

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

  3. Как предотвратить вызов методов дескриптора?

    Copy code

    Для этого нужно обратиться к атрибуту напрямую через object.__dict__.

  4. Какие встроенные дескрипторы есть в Python?

    Например, @property, @classmethod, @staticmethod.

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

Выводы

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

  • Понимание протокола дескрипторов и специальных методов
  • Реализация нужного поведения в методах __get__(), __set__(), __delete__()
  • Правильное применение дескрипторов там, где требуется их гибкость

Используя дескрипторы возможно существенно улучшить структуру и качество кода на Python. Удачи в освоении этого мощного инструмента!

Проверка типов с помощью дескрипторов

Одно из распространенных применений дескрипторов - это проверка типов атрибутов класса. Рассмотрим, как можно реализовать такую проверку:

 class TypeChecked: def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, obj, cls): value = getattr(obj, self.name) return value def __set__(self, obj, value): if not isinstance(value, self.expected_type): raise TypeError('Expected type %s' % self.expected_type) setattr(obj, self.name, value) class Person: name = TypeChecked('name', str) age = TypeChecked('age', int) 

Теперь при попытке назначить атрибуту name значение не строкового типа будет выброшено исключение TypeError. Аналогично и для атрибута age.

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

Валидация данных через дескрипторы

Помимо проверки типов, удобно реализовывать валидацию значений через дескрипторы. Например:

 class Validated: def __init__(self, name): self.name = name def __set__(self, obj, value): if value < 0: raise ValueError('Must be >= 0') setattr(obj, self.name, value) class Order: amount = Validated('amount') 

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

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

Ленивая инициализация атрибутов

Еще одно интересное применение дескрипторов - отложенная инициализация атрибутов или lazy initialization. Пример:

 class LazyInit: def __init__(self, initializer): self.initializer = initializer self.name = None def __get__(self, obj, cls): if self.name is None: self.name = self.initializer() return self.name class Person: def __init__(self, name): self.name = name def formatted_name(self): # форматирование имени pass name = LazyInit(formatted_name) 

Здесь метод formatted_name будет вызываться только при первом обращении к атрибуту name. Это позволяет оптимизировать вычисления.

Кэширование результатов методов

Еще один полезный вариант - кэширование результатов методов с помощью дескриптора:

 class Cached: def __init__(self, method): self.method = method self.cache = None def __get__(self, obj, cls): if self.cache is None: self.cache = self.method(obj) return self.cache class Circle: def area(self): # вычисление площади pass cached_area = Cached(area) 

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

Кэширование часто используемых вычислений - еще один полезный прием с дескрипторами.

Дескрипторы только для чтения

Иногда нужно сделать атрибут класса доступным только для чтения. Это легко реализуется через дескриптор:

 class ReadOnly: def __init__(self, initial): self.value = initial def __get__(self, obj, cls): return self.value class Book: title = ReadOnly('The Hobbit') 

Теперь попытка изменить title приведет к ошибке, т.к. дескриптор ReadOnly не определяет метод __set__.

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

Дескрипторы в стандартной библиотеке

Рассмотрим примеры дескрипторов из стандартной библиотеки Python:

  • @property - дескриптор данных, позволяет использовать метод как атрибут.

  • @classmethod - дескриптор не-данных, привязывает метод к классу.

  • @staticmethod - дескриптор не-данных, привязывает функцию к классу.

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

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

Статья закончилась. Вопросы остались?
Комментарии 0
Подписаться
Я хочу получать
Правила публикации
Редактирование комментария возможно в течении пяти минут после его создания, либо до момента появления ответа на данный комментарий.