描述器HowTo指南

作者:Raymond Hettinger
联系:

摘要

定义描述器、总结协议,并演示如何调用描述器。检查自定义描述器和几个内置的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

这是它所有的内容。定义这些方法中的任何一个的对象被认为是一个描述器,并在作为一个属性被查找时可以覆盖默认行为。

如果对象定义__get__()__set__(),则它被认为是数据描述器。仅定义__get__()的描述器称为非数据描述器(它们通常用于方法,但是其他用途也是可能的)。

数据描述器和非数据描述器的不同点在于计算如何覆盖实例的字典中的键。如果实例的字典具有与数据描述器具有相同名称的键,则数据描述器优先。如果实例的字典具有与非数据描述器相同的名称的键,则字典的键优先。

若要建立只读数据描述器,同时定义__get__()__set__(),并让__set__()在调用时引发AttributeError定义__set__()方法为仅引发一个异常就足以使它成为一个数据描述器。

调用描述器

描述器可以通过其方法名直接调用。例如d.__get__(obj)

或者,更常见的是在属性访问时自动调用描述器。例如,obj.dobj的字典中查找d如果d定义方法__get__(),则根据下面列出的优先级规则调用d.__get__(obj)

调用的细节取决于obj是一个对象还是一个类。

对于对象,机制在object.__getattribute__()中,它将b.x转换为type(b).__dict__['x'].__get__(b, type(b))其通过优先级链来实现,该优先级链给数据描述器优先于实例变量,实例变量优先于非数据描述器,并且将最低优先级分配给__getattr__()(如果提供的话)。完整的C实现可以在Objects/object.c中的PyObject_GenericGetAttr()中找到。

对于类,机制在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

要记住的重点是:

super()返回的对象也有一个自定义__getattribute__()方法用于调用描述器。super(B, obj).m()调用在obj.__class__.__mro__搜索紧跟B的基类A,然后返回A.__dict__['m'].__get__(obj, B)如果不是描述器,则原样返回m如果不在字典中,m将使用object.__getattribute__()继续搜索。

实现细节位于Objects/typeobject.c中的super_getattro()中。可以在Guido的教程中找到纯Python等效实现。

上面的细节显示,用于描述器的机制嵌入在objecttypesuper()__getattribute__()当类从object派生时,或者如果它们具有提供类似功能的元类,则继承此机制。同样,类可以通过重写__getattribute__()来关闭描述器调用。

描述器示例

以下代码创建一个类,其对象是数据描述器,它为每个get或set打印一条消息。覆盖__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

协议很简单,并提供令人兴奋的可能性。几个用例是如此常见,所以它们已被打包成单独的函数调用。Properties、绑定的方法和未绑定的方法、静态方法和类方法都基于描述器协议。

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授予对单元格值的访问权限。对程序的后续改进要求在每次访问时重新计算单元;然而,程序员不想影响直接访问属性的现有客户端代码。解决方案是封装value属性为一个property数据描述器:

class Cell(object):
    . . .
    def getvalue(self, obj):
        "Recalculate cell before returning value"
        self.recalc()
        return obj._value
    value = property(getvalue)

函数和方法

Python的面向对象的特性构建在一个基于函数的环境上。使用非数据描述器,两者被无缝合并在一起。

类的字典将方法存储为函数。在类定义中,使用用于创建函数的常用工具的deflambda编写方法。与常规函数的唯一区别是第一个参数保留给对象实例。根据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']  # Stored internally as a function
<function f at 0x00C45070>
>>> D.f              # Get from a class becomes an unbound method
<unbound method D.f>
>>> d.f              # Get from an instance becomes a bound method
<bound method D.f of <__main__.D object at 0x00B18C90>>

输出表明绑定和未绑定的方法是两种不同的类型。虽然它们可以以这种方式实现,但是Objects/classobject.c中的PyMethod_Type的实际C实现是具有两种不同表示的单个对象,取决于im_self字段是否设置或为NULLNone的C等效值)。

同样,调用方法对象的效果取决于im_self字段。如果设置(意味着绑定),原始函数(存储在im_func字段中)将按第一个参数设置为实例的方式调用。如果未绑定,所有参数将不变地传递到原始函数。instancemethod_call()的实际C实现只是稍微复杂一点,因为它包括一些类型检查。

静态方法和类方法

非数据描述器提供了一个简单的机制用于将绑定函数变换为方法的常见模式。

总而言之,函数具有__get__()方法,以便在作为属性访问时可以将它们转换为方法。非数据描述器将obj.f(*args)调用转换成f(obj, *args)调用klass.f(*args)变为f(*args)

此图总结了绑定及其两个最有用的变体:

Transformation Called from an Object Called from a Class
function f(obj, *args) f(*args)
staticmethod f(*args) f(*args)
classmethod f(type(obj), *args) f(klass, *args)

静态方法返回底层函数没有更改。调用c.fC.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)

每当函数只需要一个类引用并且不关心任何底层数据,这种行为就有用了。类方法的一个用途是创建替代类构造函数。在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}

使用非数据描述器协议,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