HOWTO по дескрипторам

Автор:Раймонд Хеттингер
Контакт:<python@rcn.com>

Аннотация

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

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

Определение и введение

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

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

Дескрипторы — это мощный протокол общего назначения. Это механизм, лежащий в основе свойств, методов, статических методов, методов класса и super(). Они используются в самом Python для реализации новых классов стилей, представленных в версии 2.2. Дескрипторы упрощают базовый C-код и предлагают гибкий набор новых инструментов для повседневных программ 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__() с заполнителем, вызывающим исключение, достаточно, чтобы сделать его дескриптором данных.

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

Дескриптор может вызываться непосредственно по имени его метода. Например, 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__(), если он предоставлен. Полную реализацию C можно найти в: c:func:PyObject_GenericGetAttr() в Objects/object.c.

Для классов используется механизм 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__().

Подробности реализации находятся в: c:func:super_getattro() в Objects/typeobject.c, а эквивалент на чистом Python можно найти в Учебнике Гвидо.

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

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

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

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

    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(object):
...     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(object):
    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(object):
    "Подражает PyProperty_Type() в 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. Последующие улучшения программы требуют пересчёта ячейки при каждом доступе; однако программист не хочет влиять на существующий клиентский код, напрямую обращающийся к атрибуту. Решение состоит в том, чтобы обернуть доступ к атрибуту значения в дескриптора данных свойства:

class Cell(object):
    . . .
    def getvalue(self):
        "Пересчитать ячейку перед возвращением значения"
        self.recalc()
        return self._value
    value = property(getvalue)

Функции и методы

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

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

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

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Подражать func_descr_get() в Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

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

>>> class D(object):
...     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).

В этой таблице показаны два наиболее полезных варианта привязки:

Преобразование Вызывается из объекта Вызывается из класса
function f(obj, *args) f(*args)
staticmethod f(*args) f(*args)
classmethod f(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(object):
...     def f(x):
...         print(x)
...     f = staticmethod(f)
...
>>> E.f(3)
3
>>> E().f(3)
3

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

class StaticMethod(object):
    "Подражать PyStaticMethod_Type() в Objects/funcobject.c"

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

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

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

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

Это поведение полезно, когда у функции должна быть только ссылка на класс и не заботится о каких-либо базовых данных. Одно из применений методов классов — создание альтернативных конструкторов классов. В Python 2.3 метод классов dict.fromkeys() создаёт новый словарь из списка ключей. Эквивалент на чистом Python:

class Dict(object):
    . . .
    def fromkeys(klass, iterable, value=None):
        "Подражать dict_fromkeys() в Objects/dictobject.c"
        d = klass()
        for key in iterable:
            d[key] = value
        return d
    fromkeys = classmethod(fromkeys)

Теперь можно построить новый словарь уникальных ключей:

>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

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

class ClassMethod(object):
    "Подражать PyClassMethod_Type() в Objects/funcobject.c"

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

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc