Сообщить об ошибке.

Дескрипторы классов в Python.

Руководство по использованию дескрипторов класса.

Дескрипторы классов - это мощный протокол общего назначения. Это механизм, лежащий в основе свойств, методов, статических методов, методов класса и функции super(). Они используются в самом Python для реализации классов нового стиля. Дескрипторы упрощают базовый C-код и предлагают гибкий набор новых инструментов для повседневных программ Python.

В общем случае, дескриптор - это атрибут объекта с "привязкой поведения", доступ к которому был переопределен методами в протоколе дескриптора. Этими методами являются __get__(), __set__() и __delete__(). Если какой-либо из этих методов определен для объекта, то он называется дескриптором.

Поведение по умолчанию для доступа к атрибуту заключается в получении __get__(), установке __set__() или удалении __delete__() атрибута из словаря объекта. Например, a.x имеет цепочку поиска, начинающуюся с a.__dict__['x'], затем type(a).__dict__['x'] и продолжающуюся через базовые классы type(a), исключая метаклассы. Если искомое значение является объектом, определяющим один из методов дескриптора, то Python может переопределить поведение по умолчанию и вызвать вместо него метод дескриптора. Где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены.

Содержание:


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

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

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

Если объект определяет __set__() или __delete__(), то он считается дескриптором данных. Дескрипторы, которые определяют только __get__(), называются дескрипторами без данных, обычно они используются для методов, но возможны и другие применения.

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

Чтобы создать дескриптор данных только для чтения, определите как __get__(), так и __set__() с методом __set__(), вызывающим исключение AttributeError при вызове. Определение метода __set__() с raise, достаточно, чтобы сделать его дескриптором данных.

Вызов дескрипторов класса.

Дескриптор может быть вызван непосредственно по имени метода. Например d.__get__(obj).

Кроме того, чаще всего дескриптор вызывается автоматически при доступе к атрибутам. Например, в obj.d ищет d в словаре объекта obj. Если d определяет метод __get__(), то d.__get__(obj) вызывается в соответствии с правилами приоритета, перечисленными ниже.

Детали вызова зависят от того, является ли obj объектом или классом.

Для объектов, механизм вызова находится в object.__getattribute__(), который преобразует b.x в type(b).__dict__['x'].__get__(b, type(b)). Реализация работает через цепочку приоритетов, которая дает дескрипторам данных приоритет над переменными экземпляра, переменным экземпляра приоритет над дескрипторами без данных и назначает самый низкий приоритет __getattr__(), если это предусмотрено.

Для классов механизм вызова находится в type.__getattribute__(), который преобразует B.x в B.__dict__['x'].__get__(None, B). В чистом Python это выглядит так:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

Важно помнить следующее:

  • дескрипторы классов вызываются методом __getattribute__(),
  • переопределение __getattribute__() предотвращает автоматические вызовы дескриптора,
  • object.__getattribute__() и type.__getattribute__() выполняют разные вызовы метода __get__().
  • дескрипторы данных всегда имеют приоритет над словарями экземпляров.
  • дескрипторы, не относящиеся к данным, могут быть переопределены словарями экземпляров.

Объект, возвращаемый функцией super(), также имеет собственный метод __getattribute__() для вызова дескрипторов класса. Поиск атрибута super(B, obj).m ищет в obj.__class__.__mro__ базовый класс A, следующий сразу за B, а затем возвращает A.__dict__['m'].__get__(obj, B). Если это не дескриптор, то m возвращается без изменений. Если его нет в словаре, то m возвращается к поиску с использованием object.__getattribute__().

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

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

Следующий код создает класс, объектами которого являются дескрипторы данных, которые печатают сообщение для каждого get или set. Переопределение __getattribute__() - это альтернативный подход, который может сделать это для каждого атрибута. Однако этот дескриптор полезен для мониторинга только нескольких выбранных атрибутов:

class RevealAccess:
    """Дескриптор данных, который устанавливает и возвращает 
        значения и печатает сообщение, регистрирующее их доступ.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

>>> class MyClass:
...     x = RevealAccess(10, 'var "x"')
...     y = 5
...
>>> m = MyClass()
>>> m.x
# Retrieving var "x"
# 10
>>> m.x = 20
# Updating var "x"
>>> m.x
# Retrieving var "x"
# 20
>>> m.y
# 5

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

На протоколе дескриптора основаны: свойства, связанные методы, статические методы и методы классов!

Дескрипторы данных в классе (дескрипторы атрибутов).

Вызов функции property() - это краткий способ построения дескриптора данных, который запускает вызовы функций при доступе к атрибуту.

Его подпись:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

В документации показано типичное использование для определения управляемого атрибута x:

class C:
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

Чтобы увидеть, как функция property() реализована с точки зрения протокола дескриптора класса, вот чистый эквивалент на Python:

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Встроенная функция property() помогает всякий раз, когда пользовательский интерфейс предоставляет доступ к атрибутам, а затем последующие изменения требуют вмешательства метода.

Например, класс электронных таблиц может предоставлять доступ к значению ячейки через Cell('b10').value. Последующие усовершенствования программы требуют, чтобы ячейка пересчитывалась при каждом доступе, но программист не хочет влиять на существующий клиентский код, напрямую обращающийся к атрибуту. Решение состоит в том, чтобы обернуть доступ к атрибуту value в дескриптор данных свойства:

class Cell:
    . . .
    def getvalue(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value
    value = property(getvalue)

Классно, неправда ли?

Дескрипторы функций и методы класса.

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

Словари классов хранят методы как функции. В определении класса, методы, написанные с использованием def или лямбда-выражения, обычные инструменты для создания функции. Методы отличаются от обычных функций только тем, что первый аргумент зарезервирован для экземпляра объекта. По соглашению Python ссылка на экземпляр называется self, но может называться этим или любым другим именем переменной.

Для поддержки вызовов методов, функции включают метод __get__() для привязки методов во время доступа к атрибутам. Это означает, что все функции, которые возвращают связанные методы, когда они вызываются из объекта - не являются дескрипторами данных.

В чистом Python это работает так:

class Function:
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

Запуск интерпретатора показывает, как дескриптор функции работает на практике:

>>> class D:
...     def f(self, x):
...         return x
...
>>> d = D()

# Доступ через словарь классов не вызывает __get__.
# Он просто возвращает базовый объект функции.
>>> D.__dict__['f']
# <function D.f at 0x00C45070>

# Доступ из класса вызывает функцию __get__(), 
# которая просто возвращает базовую функцию без изменений.
>>> D.f
# <function D.f at 0x00C45070>

# Функция имеет атрибут `__qualname__` для поддержки интроспекции
>>> D.f.__qualname__
# 'D.f'

# Доступ из экземпляра вызывает функцию __get__(), которая 
# возвращает функцию, обернутую в связанный объект метода
>>> d.f
# <bound method D.f of <__main__.D object at 0x00B18C90>>

# Внутренне связанный метод хранит базовую функцию 
# и связанный экземпляр.
>>> d.f.__func__
# <function D.f at 0x1012e5ae8>
>>> d.f.__self__
# <__main__.D object at 0x1012e1f98>

Дескрипторы, не относящиеся к данным.

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

Напомним, что у функций есть метод __get__(), так что они могут быть преобразованы в метод при доступе, как и атрибуты. Дескриптор, не относящийся к данным, преобразует вызов obj.f(*args) в f(obj, *args). Вызов klass.f(*args) - становится f(*args).

На этой диаграмме представлена ​​привязка и два ее наиболее полезных варианта:

ПреобразованияВызов из объектаВызов из класса
functionf(obj, *args)f(*args)
staticmethodf(*args)f(*args)
classmethodf(type(obj), *args)f(klass, *args)

Статический метод как дескриптор без данных.

Статические методы возвращают базовую функцию без изменений. Вызов c.f или C.f эквивалентен прямому поиску в object.__getattribute__(c, 'f') или в object.__getattribute__(C, "f"). В результате функция становится одинаково доступной как из объекта, так и из класса.

Хорошими кандидатами на статические методы являются методы, которые не ссылаются на переменную self.

Например, какая нибудь библиотека статистики может включать класс контейнера для экспериментальных данных. Класс предоставляет обычные методы для вычисления среднего, медианы и других описательных статистических данных, которые зависят от данных. Однако могут существовать полезные функции, которые концептуально связаны, но не зависят от данных. Например, erf(x) - это удобная процедура преобразования, которая появляется в статистической работе, но не зависит напрямую от конкретного набора данных. Она может быть вызвана либо из объекта, либо из класса: s.erf(1.5) --> .9332 или Sample.erf(1.5) --> .9332.

Так как статические методы возвращают базовую функцию без изменений, то вызовы примеров неинтересны:

>>> class E:
...     def f(x):
...         print(x)
...     f = staticmethod(f)
...
>>> E.f(3)
# 3
>>> E().f(3)
# 3

При использовании протокола дескриптора не относящегося к данным, чистая версия staticmethod() для Python будет выглядеть так:

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

Метод класса как дескриптор без данных.

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

>>> class E:
...     def f(klass, x):
...         return klass.__name__, x
...     f = classmethod(f)
...
>>> print(E.f(3))
('E', 3)
>>> print(E().f(3))
('E', 3)

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