Руководство по описателям (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-реализация находится в 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__().
Пример описателя
Следующий код создаёт класс, объекты которого являются описателями данных, печатающими сообщение при каждом чтении или записи. Переопределение __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. Последующие улучшения программы требуют перерасчёта ячейки при каждом доступе, но программист не хочет влиять на существующий клиентский код, напрямую обращающийся к атрибуту. Решение — обернуть доступ к атрибуту value в описатель свойств:
class Cell(object):
. . .
def getvalue(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
value = property(getvalue)
Функции и методы
Объектно-ориентированные возможности Python построены на основе функциональной среды. Используя описатели без данных, они seamlessly объединяются.
Словари классов хранят методы как функции. В определении класса методы пишутся с помощью 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 and # the bound instance. >>> 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).
В этой таблице обобщается связывание и два наиболее полезных варианта:
Преобразование | Вызов из объекта | Вызов из класса |
|---|---|---|
функция | 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.
Поскольку staticmethods возвращают базовую функцию без изменений, примеры вызовов неинтригующие:
>>> 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
© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/howto/descriptor.html