描述符操作指南 — Python 文档

来自菜鸟教程
Python/docs/3.7/howto/descriptor
跳转至:导航、​搜索

描述符操作指南

作者
雷蒙德·赫廷格
接触

抽象的

定义描述符,总结协议,并显示如何调用描述符。 检查自定义描述符和多个内置 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 实现可以在 :source:`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

要记住的要点是:

  • 描述符由 __getattribute__() 方法调用
  • 覆盖 __getattribute__() 防止自动描述符调用
  • object.__getattribute__()type.__getattribute__()__get__() 进行不同的调用。
  • 数据描述符总是覆盖实例字典。
  • 非数据描述符可能会被实例字典覆盖。

super() 返回的对象也有一个自定义的 __getattribute__() 方法来调用描述符。 调用 super(B, obj).m()B 之后立即搜索 obj.__class__.__mro__ 的基类 A,然后返回 A.__dict__['m'].__get__(obj, B)。 如果不是描述符,则返回 m 不变。 如果不在字典中,m 将恢复为使用 object.__getattribute__() 的搜索。

实现细节在 :source:`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

该协议很简单,并提供了令人兴奋的可能性。 几个用例非常常见,以至于它们被打包成单独的函数调用。 属性、绑定方法、静态方法和类方法都基于描述符协议。


特性

调用 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 的面向对象特性建立在基于函数的环境之上。 使用非数据描述符,两者无缝合并。

类字典将方法存储为函数。 在类定义中,方法是使用 deflambda 编写的,它们是创建函数的常用工具。 方法与常规函数的区别仅在于第一个参数是为对象实例保留的。 根据 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,
# 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'>

静态方法和类方法

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

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

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

转型 从对象调用 从类调用
功能 f(obj, *args) f(*参数)
静态方法 f(*参数) f(*参数)
类方法 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

由于 staticmethods 没有更改地返回底层函数,因此示例调用并不令人兴奋:

>>> class E(object):
...     def f(x):
...         print(x)
...     f = staticmethod(f)
...
>>> E.f(3)
3
>>> 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)

当函数只需要一个类引用而不关心任何底层数据时,这种行为就很有用。 classmethods 的一种用途是创建备用类构造函数。 在 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