Что такое дескрипторы и их использование в Python 3.6+

Spread the love

Что такое дескрипторы? Очень частый вопрос на собеседованиях. Сложность вопроса в том что реально в своих проектах почти ни кто не использует дескрипторы. Вы можете проработать все жизнь программистом python и ни разу не задействовать их ни в одном своем проекте. Но при этом вы будете почти постоянно использовать их через подключаемые сторонние библиотеки. Обычно говориться если вы захотели использовать их, остановитесь и лучше подумайте об архитектуре проекта. Возможно существует множество других более простых решений. И в большинстве случаев так и будет, за не большим исключением. Дескрипторы традиционно используются только если вы создаете ORM или новый фреймворк. Поэтому знать о них нужно, но не столько для их использования, а больше для понимания как работает магия python.

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

Вы видели похожий код или, может быть даже писали что-то подобное?

from sqlalchemy import Column, Integer, String

class User(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String)

Этот небольшой фрагмент был частично взят из учебника по популярной ORM библиотеки SQLAlchemy. Подобный код можно встреть наверно в любой ORM в python. А вы когда-нибудь задумывались, почему атрибуты id и name не передаются через метод __init__ и потом не привязываются к экземпляру класса, как это обычно делается в классе. Если да то в этой статье я расскажу, как и зачем это делается.

В начале я объясню что такое дескрипторы, зачем их использовать, как их используют в предыдущих версиях Python (<= 3.5,) и, наконец, как их использовать в Python 3.6 с новой функциональностью, описанной в PEP 487 – Упрощенная настройка создания классов

Что такое дескрипторы?

Самое известное определение дескриптора дано Раймондом Хеттингером в руководстве Descriptor HowTo Guide:

Дескриптор это атрибут объекта со “связанным поведением”, то есть такой атрибут, при доступе к которому его поведение переопределяется методом протокола дескриптора. Эти методы  __get____set__ и __delete__. Если хотя бы один из этих методов определен в объекте , то можно сказать что этот метод дескриптор.

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

Попробуем рассказать о дескрипторах чуть проще. В python существует три варианта доступа к атрибуту. Допустим у нас есть атрибут a объекта obj:

  1. Получим значение атрибута, some_variable = obj.a
  2. Изменим его значение, obj.a = 'new value'
  3. Удалим атрибут, del obj.a

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

Зачем нам нужны дексрипторы?

Давайте рассмотрим пример:

class Order:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def total(self):
        return self.price * self.quantity

apple_order = Order('apple', 1, 10)
apple_order.total()
# 10

Что не так с этим кодов? Если этот код начать использовать, мы столкнемся с проблемой. Наши данные ни как не проверяются. То есть цена (price) и количество (quantity) может принимать любое значение:

apple_order.quantity = -10
apple_order.total
# -10, too good of a deal!

Вместо того чтобы использовать методы getter и setter и создавать новое API, давайте используем стандартный декоратор property для проверки значения атрибута quantity:

class Order:
    def __init__(self, name, price, quantity):
        self._name = name
        self.price = price
        self._quantity = quantity  # (1)

    @property
    def quantity(self):
        return self._quantity

    @quantity.setter
    def quantity(self, value):
        if value < 0:
            raise ValueError('Cannot be negative.')
        self._quantity = value  # (2)
    ...

apple_order.quantity = -10
# ValueError: Cannot be negative

Мы преобразовали quantity из простого атрибута в неотрицательное (non-negative) свойство (property). Обратите внимание на строку (1) где мы переименовали атрибут в _quantity что бы избежать получение в строке (2) ошибки RecursionError.

И это все что нам надо было сделать? Конечно нет. Мы забыли об атрибуте  price, который также не может быть отрицательным. Возможно, первым делом вы пытаетесь просто скопировать то что мы делали для атрибута  _quantity. А что если у нас будет двадцать таких атрибутов. Помните принцип DRY: когда вы обнаруживаете, что делаете одно и то же дважды, это хороший признак для удаления повторно используемого кода. Давайте посмотрим, чем нам в этом случае могут помочь дескрипторы.

Как использовать дескрипторы

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

class Order:
    price = NonNegative('price')  # (3)
    quantity = NonNegative('quantity')

    def __init__(self, name, price, quantity):
        self._name = name
        self.price = price
        self.quantity = quantity

    def total(self):
        return self.price * self.quantity

apple_order = Order('apple', 1, 10)
apple_order.total()
# 10
apple_order.price = -10
# ValueError: Cannot be negative
apple_order.quantity = -10
# ValueError: Cannot be negative

Обратите внимание на атрибуты класса определенные до метода __init__ Это очень похоже на пример от SQLAlchemy приведенный в начале статьи. Теперь нам нужно создать класс NonNegative и реализовать протокол дескрипторов:

class NonNegative:
    def __init__(self, name):
        self.name = name  # (4)
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]  # (5)
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Cannot be negative.')
        instance.__dict__[self.name] = value  # (6)

Строка (4): атрибут name необходим при создание объекта NonNegative в строке (3), в данные момент еще не происходит изменения значения price. Таким образом, мы явно передаем имя атрибута price что бы использовать его как ключ при доступе к экземпляру __dict__.

Позже мы увидим, как в Python 3.6+ мы можем избежать текущей избыточности кода.

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

Строки (5) и (6): вместо использования встроенный функций getattr и setattr, мы напрямую обращаемся к объекту __dict__ , что бы избежать ошибки  RecursionError, так как обращение через встроенные функции будут так же перехватываться протоколом дескрипторов.

Добро пожаловать в Python 3.6+

У нас все еще есть избыточность кода в строке (3). Как нам написать более чистое API по типу:

class Order:
    price = NonNegative()
    quantity = NonNegative()

    def __init__(self, name, price, quantity):
        ...

Давайте используем новый протокол дескрипторов появившийся в Python 3.6:

  • object.__set_name__(self, owner, name)
    • Вызывается во время создания класса. В этом случае дескриптор назначается на имя атрибута.

С этим протоколом, мы можем удалить __init__ и привязать имя атрибута к дескриптору:

class NonNegative:
    ...
    def __set_name__(self, owner, name):
        self.name = name

Теперь окончательная версия нашего кода:

class NonNegative:
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Cannot be negative.')
        instance.__dict__[self.name] = value
    def __set_name__(self, owner, name):
        self.name = name

class Order:
    price = NonNegative()
    quantity = NonNegative()

    def __init__(self, name, price, quantity):
        self._name = name
        self.price = price
        self.quantity = quantity

    def total(self):
        return self.price * self.quantity

apple_order = Order('apple', 1, 10)
apple_order.total()
# 10
apple_order.price = -10
# ValueError: Cannot be negative
apple_order.quantity = -10
# ValueError: Cannot be negative

Заключение

Python – это язык программирования общего назначения. Мне нравится, что он не только обладает очень мощными функционалом, которые очень гибок (например, использование мета-классов), а также имеет высокоуровневое API для решения 99% потребностей (например, те же дескрипторов). Дескрипторы, безусловно, являются хорошим инструментом для привязки поведения к атрибутам. Хотя метаклассы потенциально могут делать то же самое, дескриптор может решить проблему более изящно.


Spread the love

Что такое дескрипторы и их использование в Python 3.6+: 1 комментарий

  • 18.09.2019 в 22:37
    Permalink

    Действительно, только с этой статьёй как-то всё легко просто и понятно стало с дескрипторами.
    Спасибо!

    Ответ

Добавить комментарий

Ваш e-mail не будет опубликован.