Содержание страницы
Руководство по дескрипторам¶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 может переопределить поведение по умолчанию и вместо этого вызвать метод дескриптора. То, где это происходит в цепочке приоритетов, зависит от того, какие методы дескриптора были определены. Обратите внимание, что дескрипторы вызываются только для объектов или классов нового стиля (класс является классом нового стиля, если он наследуется от object или type).
Дескрипторы – это мощный универсальный протокол. Они лежат в основе свойств, методов, статических методов, методов класса и super(). Они используются в самом Python для реализации классов нового стиля, появившихся в версии 2.2. Дескрипторы упрощают внутренний C-код и предоставляют гибкий набор новых инструментов для повседневных программ на Python.
Протокол дескриптора¶Descriptor Protocol
descr.__get__(self, obj, type=None) --> значение
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
Вот и всё. Определите любой из этих методов – и объект считается дескриптором и может переопределять поведение по умолчанию при поиске в качестве атрибута.
Если объект определяет и __get__(), и __set__(), он считается дескриптором данных. Дескрипторы, определяющие только __get__(), называются дескрипторами не-данных (обычно они используются для методов, но возможны и другие применения).
Дескрипторы данных и не-данных различаются тем, как вычисляются переопределения по отношению к записям в словаре экземпляра. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора данных, дескриптор данных имеет приоритет. Если в словаре экземпляра есть запись с тем же именем, что и у дескриптора не-данных, приоритет имеет запись словаря.
Чтобы сделать дескриптор данных доступным только для чтения, определите и __get__(), и __set__(), причём __set__() должен возбуждать AttributeError при вызове. Определения __set__() с заглушкой, возбуждающей исключение, достаточно, чтобы сделать его дескриптором данных.
Вызов дескрипторов¶Invoking Descriptors
Дескриптор можно вызвать напрямую по имени метода. Например, d.__get__(obj).
Но чаще дескриптор вызывается автоматически при доступе к атрибуту. Например, obj.d ищет d в словаре obj. Если d определяет метод __get__(), то вызывается d.__get__(obj) в соответствии с правилами приоритета, приведёнными ниже.
Подробности вызова зависят от того, является ли obj объектом или классом. В любом случае дескрипторы работают только с объектами и классами нового стиля. Класс является классом нового стиля, если он является подклассом object.
Для объектов механизм находится в 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__() предотвращает автоматический вызов дескрипторов
- __getattribute__() доступен только для классов и объектов нового стиля
- object.__getattribute__() и type.__getattribute__() по-разному вызывают __get__().
- дескрипторы данных всегда переопределяют словари экземпляров.
- дескрипторы без данных могут быть переопределены словарями экземпляров.
Объект, возвращаемый super(), также имеет собственный метод __getattribute__() для вызова дескрипторов. Вызов super(B, obj).m() ищет в obj.__class__.__mro__ базовый класс A, следующий непосредственно за B, и затем возвращает A.__dict__['m'].__get__(obj, A). Если это не дескриптор, m возвращается без изменений. Если его нет в словаре, m возвращается к поиску с помощью object.__getattribute__().
Обратите внимание: в Python 2.2 super(B, obj).m() вызывал бы __get__(), только если m был дескриптором данных. В Python 2.3 дескрипторы не данных также вызываются, если только не задействован класс старого стиля. Подробности реализации находятся в super_getattro() в Objects/typeobject.c, а чистый Python-эквивалент можно найти в руководстве Гвидо.
Из изложенного видно, что механизм дескрипторов встроен в методы __getattribute__() для object, type и super(). Классы наследуют этот механизм, если они производны от object или имеют метакласс, предоставляющий аналогичную функциональность. Классы также могут отключить вызов дескрипторов, переопределив __getattribute__().
Пример дескриптора¶Descriptor Example
Следующий код создаёт класс, объекты которого являются дескрипторами данных, выводящими сообщение при каждом get или set. Переопределение __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
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)
Встроенная функция property() полезна всякий раз, когда пользовательский интерфейс предоставил доступ к атрибуту, а последующие изменения потребовали вмешательства метода.
Например, класс электронной таблицы может предоставлять доступ к значению ячейки через Cell('b10').value. Последующие улучшения программы требуют пересчитывать ячейку при каждом доступе; однако программист не хочет затрагивать существующий клиентский код, обращающийся к атрибуту напрямую. Решение – обернуть доступ к атрибуту value в дескриптор данных property:
class Cell(object):
. . .
def getvalue(self, obj):
"Recalculate cell before returning value"
self.recalc()
return obj._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"
return types.MethodType(self, obj, objtype)
Запуск интерпретатора показывает, как дескриптор функции работает на практике:
>>> 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 (эквивалент None на C).
Аналогично, результат вызова объекта метода зависит от поля 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
С использованием протокола дескриптора не данных, чистая 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)
Такое поведение полезно, когда функции нужна только ссылка на класс и ей не важны какие-либо базовые данные. Одно из применений classmethod – создание альтернативных конструкторов класса. В Python 2.3 classmethod dict.fromkeys() создаёт новый словарь из списка ключей. Чистая реализация на Python выглядит так:
class Dict:
. . .
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