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

Автор:Raymond Hettinger
Контакт:<python at rcn dot 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 может быть найдено in:c:func:PyObject_GenericGetAttr() в Objects/object.c.

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

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__().
  • данные дескрипторы всегда переопределяют словари сущность.
  • non-data дескрипторы может быть переопределен сущность словарями.

Объект, возвращенный 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 или если у них есть meta-класс, обеспечивающие аналогичную функциональность. Аналогично, классы может отключать вызов дескриптор путем переопределения __getattribute__().

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

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

class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    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):
    "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)

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

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

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

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

class Function(object):
    . . .
    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(object):
...     def f(self, x):
...         return x
...
>>> d = D()

# Access through the class dictionary does not invoke __get__.
# It just returns the underlying function object.
>>> D.__dict__['f']
<function D.f at 0x00C45070>

# Dotted access from a class calls __get__() which just returns
# the underlying function unchanged.
>>> D.f
<function D.f at 0x00C45070>

# The function has a __qualname__ attribute to support introspection
>>> D.f.__qualname__
'D.f'

# Dotted access from an instance calls __get__() which returns the
# function wrapped in a bound method object
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>

# Internally, the bound method stores the underlying function,
# the bound instance, and the class of the bound instance.
>>> d.f.__func__
<function D.f at 0x1012e5ae8>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
>>> d.f.__class__
<class 'method'>

Статические методы и методы классов

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

Для повторного отображения функции имеют __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):
    "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)

Такое поведение полезно всякий раз, когда функция должна иметь только ссылку на класс и не заботится о базовых данных. Одним из способов использования классовых методов является создание альтернативных конструкторов класс. В Python 2.3 классметод 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}

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

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