On this page
Descriptors 操作指南
Author
- Raymond Hettinger
Contact
Contents
Abstract
定义 Descriptors,总结协议,并说明如何调用 Descriptors。检查一个自定义 Descriptors 和几个内置的 PythonDescriptors,包括函数,属性,静态方法和类方法。pass提供一个纯 Python 等效项和一个示例应用程序,展示每种方法的工作原理。
了解 Descriptors 不仅可以访问更大的工具集,还可以使人们对 Python 的工作方式有更深入的了解,并赞赏其设计的优雅。
定义和简介
通常,Descriptors 是具有“绑定行为”的对象属性,该对象属性的访问已被 Descriptors 协议中的方法覆盖。这些方法是get(),set()和delete()。如果为对象定义了这些方法中的任何一种,则称其为 Descriptors。
属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x
的查找链从a.__dict__['x']
开始,然后是type(a).__dict__['x']
,并一直到type(a)
的 Base Class(不包括元类)continue。如果查找到的值是定义 Descriptors 方法之一的对象,则 Python 可能会覆盖默认行为并改为调用 Descriptors 方法。优先链在何处发生取决于定义了哪些 Descriptors 方法。
Descriptors 是Function强大的通用协议。它们是属性,方法,静态方法,类方法和super()背后的机制。在 Python 本身中使用它们来实现 2.2 版中引入的新样式类。Descriptors 简化了基础 C 代码,并为日常 Python 程序提供了一组灵活的新工具。
Descriptor Protocol
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
这就是全部。定义任何这些方法,然后将对象视为 Descriptors,并且在被视为属性时可以覆盖默认行为。
如果对象定义set()或delete(),则将其视为数据 Descriptors。仅定义get()的 Descriptors 称为非数据 Descriptors(它们通常用于方法,但也可以用于其他用途)。
数据和非数据 Descriptors 的区别在于,如何计算实例字典中条目的替代值。如果实例的字典具有与数据 Descriptors 同名的条目,则数据 Descriptors 优先。如果实例的字典具有与非数据 Descriptors 同名的条目,则该字典条目优先。
要创建只读数据 Descriptors,请定义get()和set(),并在调用时将set()引发AttributeError。用引发异常的占位符定义set()方法足以使其成为数据 Descriptors。
Invoking Descriptors
Descriptors 可以pass其方法名称直接调用。例如d.__get__(obj)
。
或者,更常见的是在属性访问时自动调用 Descriptors。例如,obj.d
在obj
的字典中查找d
。如果d
定义了方法get(),则根据下面列出的优先级规则调用d.__get__(obj)
。
调用的详细信息取决于obj
是对象还是类。
对于对象,机器位于object.getattribute()中,它将b.x
转换为type(b).__dict__['x'].__get__(b, type(b))
。该实现pass优先级链进行工作,该优先级链赋予数据 Descriptors 优先于实例变量的优先级,实例变量优先于非数据 Descriptors 的优先级,并为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()方法调用 Descriptors
覆盖getattribute()可防止自动 Descriptors 调用
object.getattribute()和
type.__getattribute__()
对get()进行了不同的调用。数据 Descriptors 始终会覆盖实例字典。
非数据 Descriptors 可以被实例字典覆盖。
super()
返回的对象还具有用于调用 Descriptors 的自定义getattribute()方法。属性查找super(B, obj).m
在B
之后立即在obj.__class__.__mro__
中搜索 Base ClassA
,然后返回A.__dict__['m'].__get__(obj, B)
。如果不是 Descriptors,则m
不变返回。如果不在词典中,则m
会使用object.getattribute()恢复搜索。
实现细节在Objects/typeobject.c的super_getattro()
中。可以在Guido's Tutorial中找到与 Python 等效的纯文本。
上面的详细信息表明,Descriptors 的机制已嵌入object,type和super()的getattribute()方法中。当类从object派生或具有提供类似Function的元类时,它们将继承此机制。同样,类可以pass重写getattribute()来关闭 Descriptors 调用。
Descriptor Example
下面的代码创建一个类,其对象是数据 Descriptors,该 Descriptors 为每个 get 或 set 打印一条消息。替代getattribute()是可以对每个属性执行此操作的替代方法。但是,此 Descriptors 对于仅监视几个选定的属性很有用:
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
该协议很简单,并提供了令人兴奋的可能性。几种用例非常普遍,以至于它们被打包到单独的函数调用中。属性,绑定方法,静态方法和类方法均基于 Descriptors 协议。
Properties
调用property()是构建数据 Descriptors 的简洁方法,该 Descriptors 在访问属性时触发函数调用。它的签名是:
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.")
要了解如何根据 Descriptors 协议实现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()会在用户界面授予属性访问权限时提供帮助,然后随后的更改需要方法的干预。
例如,电子表格类可以passCell('b10').value
授予对单元格值的访问权限。对程序的后续改进要求每次访问都要重新计算单元格;但是,程序员不希望影响直接访问该属性的现有 Client 端代码。解决方案是将对 value 属性的访问包装在属性数据 Descriptors 中:
class Cell(object):
. . .
def getvalue(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
value = property(getvalue)
函数和方法
Python 的面向对象Function是基于函数的环境构建的。使用非数据 Descriptors,将两者无缝合并。
类字典将方法存储为函数。在类定义中,使用创建函数的常用工具def或lambda编写方法。方法与常规函数的不同之处仅在于第一个参数是为对象实例保留的。按照 Python 约定,实例引用称为* self ,但也可以称为 this *或任何其他变量名称。
为了支持方法调用,函数包括get()方法,用于在属性访问期间绑定方法。这意味着所有函数都是非数据 Descriptors,当从对象调用它们时,它们返回绑定方法。在纯 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)
运行解释器显示了函数 Descriptors 在实践中的工作方式:
>>> 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'>
静态方法和类方法
非数据 Descriptors 为将函数绑定到方法的通常模式提供了一种简单的机制。
概括地说,函数具有get()方法,以便在作为属性访问时可以将它们转换为方法。非数据 Descriptors 将obj.f(*args)
调用转换为f(obj, *args)
。呼叫klass.f(*args)
变为f(*args)
。
下表总结了绑定及其两个最有用的变体:
Note
Transformation | 从对象调用 | 从类召集 |
---|---|---|
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
变量的方法。
例如,统计数据包可以包括用于实验数据的容器类。该类提供了用于计算依赖于数据的平均值,均值,中位数和其他描述性统计信息的常规方法。但是,可能存在有用的Function,这些Function在概念上相关但不依赖于数据。例如,erf(x)
是方便的转换例程,它在统计工作中出现,但不直接依赖于特定的数据集。可以从对象或类s.erf(1.5) --> .9332
或Sample.erf(1.5) --> .9332
调用它。
由于 staticmethod 不变地返回了基础函数,因此示例调用并不令人兴奋:
>>> class E(object):
... def f(x):
... print(x)
... f = staticmethod(f)
...
>>> E.f(3)
3
>>> E().f(3)
3
使用非数据 Descriptors 协议,纯 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}
使用非数据 Descriptors 协议,纯 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