Содержание страницы
Руководство по дескрипторам¶Descriptor HowTo Guide
- Автор
Рэймонд Хеттингер
- Контакт
<python at rcn dot com>
Содержание
-
Определение и введениеDefinition and Introduction
Протокол дескрипторовDescriptor Protocol
Вызов дескрипторовInvoking Descriptors
Пример дескриптораDescriptor Example
СвойстваProperties
Функции и методыFunctions and Methods
Статические методы и методы классаStatic Methods and Class Methods
Аннотация¶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
Вызов 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__() для привязки методов при доступе к атрибутам. Это означает, что все функции являются дескрипторами не-данных, которые возвращают связанные методы при вызове от объекта. На чистом Python это работает так:
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'>
Статические методы и методы класса¶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