Skip to main content

描述符HowTo Guide

Author:

Raymond Hettinger

Contact:

<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

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

如果对象定义 __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()B 之后立即搜索 obj.__class__.__mro__ 以获得基本类 A,然后返回 A.__dict__['m'].__get__(obj, B)。如果不是描述符,则返回 m 不变。如果不在字典中,m 将回复到使用 object.__getattribute__() 的搜索。

实施细节在 super_getattro()Objects/typeobject.c。并且可以在 Guido’s Tutorial 中找到纯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

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

属性

调用 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, 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 字段是设置还是是 NULL (与 None 相等的C)。

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

静态方法和类方法

非数据描述符提供了用于将绑定函数的通常模式变化为方法的简单机制。

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

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

转型

从对象调用

从类调用

功能

f(obj,* args)

f(* args)

静态方法

f(* args)

f(* args)

类方法

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) --> .9332Sample.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