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.dobj的字典中查找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).mB之后立即在obj.__class__.__mro__中搜索 Base ClassA,然后返回A.__dict__['m'].__get__(obj, B)。如果不是 Descriptors,则m不变返回。如果不在词典中,则m会使用object.getattribute()恢复搜索。

实现细节在Objects/typeobject.csuper_getattro()中。可以在Guido's Tutorial中找到与 Python 等效的纯文本。

上面的详细信息表明,Descriptors 的机制已嵌入objecttypesuper()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,将两者无缝合并。

类字典将方法存储为函数。在类定义中,使用创建函数的常用工具deflambda编写方法。方法与常规函数的不同之处仅在于第一个参数是为对象实例保留的。按照 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从对象调用从类召集
functionf(obj, *args)f(*args)
staticmethodf(*args)f(*args)
classmethodf(type(obj), *args)f(klass, *args)

静态方法返回基础函数,不做任何更改。调用c.fC.f等效于直接查询object.__getattribute__(c, "f")object.__getattribute__(C, "f")。结果,该函数变得可以从对象或类进行相同的访问。

静态方法的最佳候选方法是不引用self变量的方法。

例如,统计数据包可以包括用于实验数据的容器类。该类提供了用于计算依赖于数据的平均值,均值,中位数和其他描述性统计信息的常规方法。但是,可能存在有用的Function,这些Function在概念上相关但不依赖于数据。例如,erf(x)是方便的转换例程,它在统计工作中出现,但不直接依赖于特定的数据集。可以从对象或类s.erf(1.5) --> .9332Sample.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