Документация Python неофициальный перевод
Содержание страницы

Руководство по дескрипторамDescriptor HowTo Guide

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

АннотацияAbstract

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

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

Определение и введениеDefinition and Introduction

В общем случае дескриптор – это атрибут объекта с «поведением связывания», доступ к которому переопределён методами протокола дескриптора. Такими методами являются __get__(), __set__() и __delete__(). Если для объекта определён хотя бы один из этих методов, он считается дескриптором.

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

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

Протокол дескриптораDescriptor Protocol

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

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

descr.__delete__(self, obj) --> None

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

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

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

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

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

Дескриптор можно вызвать напрямую по имени его метода. Например, 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 можно найти в 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__().

Детали реализации находятся в super_getattro() в Objects/typeobject.c. Эквивалент на чистом Python можно найти в руководстве Гвидо.

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

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

Следующий код создаёт класс, объекты которого являются дескрипторами данных, выводящими сообщение при каждом получении или установке значения. Переопределение __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

Протокол прост и открывает захватывающие возможности. Некоторые сценарии использования настолько распространены, что были упакованы в отдельные вызовы функций. Свойства (properties), связанные (bound) и несвязанные (unbound) методы, статические методы и методы класса – все основаны на протоколе дескрипторов.

СвойстваProperties

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

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

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

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

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

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

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

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

>>> class D(object):
...     def f(self, x):
...         return x
...
>>> d = D()
>>> D.__dict__['f']  # Внутренне хранится как функция
<function f at 0x00C45070>
>>> D.f              # При получении из класса становится несвязанным методом
<unbound method D.f>
>>> d.f              # При получении из экземпляра становится связанным методом
<bound method D.f of <__main__.D object at 0x00B18C90>>

Вывод предполагает, что связанные и несвязанные методы – это два разных типа. Хотя их можно было реализовать таким образом, фактическая C-реализация PyMethod_Type в Objects/classobject.c представляет собой единый объект с двумя разными представлениями в зависимости от того, установлено ли поле im_self или равно NULL (C-эквивалент None).

Аналогично, эффект вызова объекта метода зависит от поля im_self. Если оно установлено (то есть метод связан), исходная функция (хранящаяся в поле im_func) вызывается как обычно, с первым аргументом, установленным на экземпляр. Если не связано, все аргументы передаются исходной функции без изменений. Фактическая C-реализация instancemethod_call() лишь немного сложнее тем, что включает некоторую проверку типов.

Статические методы и методы классаStatic Methods and Class Methods

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

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

Эта таблица обобщает привязку и два наиболее полезных её варианта:

Преобразование Вызов из объекта Вызов из класса
функция 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)
...
>>> print(E.f(3))
3
>>> print(E().f(3))
3

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

class StaticMethod(object):
    "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(object):
...     def f(klass, x):
...         return klass.__name__, x
...     f = classmethod(f)
...
>>> print(E.f(3))
('E', 3)
>>> print(E().f(3))
('E', 3)

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

class Dict(object):
    . . .
    def fromkeys(klass, iterable, value=None):
        "Emulate dict_fromkeys() in 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}

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

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in 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