Python Descriptors 指南

原文地址:http://users.rcn.com/python/download/Descriptor.htm

约定
descriptor:描述符
data descriptor:数据描述符
non-data descriptor:非数据描述符
object attribute:对象属性(包括 class instance 和 class object)
attribute access:属性访问
method:方法
function:函数

摘要

文章对描述符的定义以及描述符协议做了描述,并且展示了描述符的调用方式。举例说明了一个自定义描述符以及一些 Python 内置的描述符,包括:functions, properties, static methods 和 class methods。通过用纯 Python 代码编码来展示它们内部实际的运行方式。

学习描述符不仅仅能帮我们接触到更大型的 Python 工具集,也有助于我们更深入理解 Python 的运行机理,同时会感叹它优雅的设计理念。

定义和介绍

一般来说,描述符就是一个被绑定了某种行为的对象属性,这个对象属性的访问方式由描述符协议中定义的方法重写了。这些方法包括:__get__, __set____delete__,如果某个对象定义了其中任何一个方法,这个对象就可以称之为描述符。

Python 中一个对象的属性访问方式默认有三种:get, set 和 delete,并且是通过对象的属性字典来访问。例如:a.x 的查找链开始于 a.__dict__['x'],接着是 type(a).__dict__['x'],接着是 type(a) 的基类中的属性这样继续下去,除了 metaclasses。如果被查找的值是一个定义了描述符协议中提到的方法的对象,那么 Python 可能会重写(override)属性查找的默认行为,转而调用描述符方法。至于具体的调用过程取决于对象定义了哪种描述符方法。需要注意的是,描述符特性只适用于 new style objects/classes(继承自 object 或者 type 的新式类)

描述符协议是一个强大的、通用协议。Python 的 properties, methods, static methods, class methods 和 super() 底层的运行机制就跟描述符协议有关。描述符被广泛用于 Python 自身来实现新式类(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 异常,这样就足够了。

调用描述符

一个描述符能通过自身的方法直接调用,例如:d.__get__(obj),不过更常用的是通过属性访问来被自动调用。例如:obj.d 会在 obj 的字典中找 d,如果 d 定义了 __get__,那么 d.__get__(obj) 就会被调用。具体调用流程取决于 obj 是 object 还是 class,但不管是哪种,描述符只在新式类(objects and classes)中有效

对于 objects,转换机制在 object.__getattribute__ 方法中,它将 b.x 转化成 type(b).__dict__['x'].__get__(b, type(b))。在 Python 内部实现的查找链中,数据描述符的优先级比实例变量的优先级高,实例变量的优先级比非数据描述符的优先级高,而 __getattr__ 方法的优先级最低(如果定义了这个方法的话)。具体的 C 代码实现在 Objects/object.cPyObject_GenericGetAttr() 函数中

对于 classes,转化机制在 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__ 方法会阻止描述符的自动调用
  • __getattribute__ 方法只在新式类(classes 和 objects)中存在
  • object.__getattribute__type.__getattribute__ 调用 __get__ 的方式不同
  • 数据描述符优先级高于实例变量,实例变量优先级高于非数据描述符

super() 方法返回的对象也有个定制的 __getattribute__ 方法调用描述符。super(B, obj).m() 调用会按照 obj.__class__.__mro__ 查找 B 类的基类 A,并且返回 A.__dict__['m'].__get__(obj, A)。如果 m 不是一个描述符,m 按原样返回。如果 m 不在 A 的字典里,m 转为如下的查找方式:object.__getattribute__

注意,Python 2.2 的时候,如果 m 是个数据描述符 super(B, obj).m() 只会调用 __get__ 方法。Python 2.3 的时候,如果是个非数据描述符也会被调用。以上的实现细节在源代码:Objects/typeobject.c 的方法:super_getattro() 中,类似的 Python 实现可以参考 Guido’s Tutorial

以上所描述的描述符机制实现嵌入在 object, type 和 super 的__getattribute__() 方法中

描述符举例

下面这段代码创建了一个 MyClass 类,它的 x 属性是一个描述符。通过对 m 对象的属性调用 m.x 来展示描述符的调用方式。

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, bound and unbound methods, static methods 和 class methods 实现都基于描述符协议

Properties

调用 property() 方法是创建一个数据描述符的简便方式。方法声明如下:
property(fget=None,fset=None,fdel=None,doc=None) -> property attribute

使用方法如下:

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
        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)

property() 方法可以做属性的安全校验或者代码的向前兼容。如下的 Cell 类,假如最开始时我能直接通过 Cell('b10').value 来操作 value 属性,如果后续我需要做一些限制并且不需要修改原来的代码逻辑,那我只要用 property() 方法来对 value 做一层包装即可

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

Functions and Methods

Python 的面向对象特性是在函数的基础上构建的,使用非数据描述符,两者能无缝结合。

类对象会把方法当做函数存储在自己的字典里。在 class 的定义中,方法是用 def 或者 lambda 定义的,和定义函数的方式类似,唯一不同的是方法的第一个参数是为实例对象保留的。按照 Python 的编码风格,实例的引用称为 self,但也可以取名为 this 或者其它的变量名。

为了支持方法调用,函数里包含了 __get__ 方法以便在属性访问的过程中将方法绑定到某个对象上。这意味着所有的函数都是非数据描述符,它们根据是否被 object 或者 class 调用而返回绑定或者非绑定方法。用 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
0x00C45070>
>>> D.f             # Get from a class becomes an unbound method

>>> d.f             # Get from an instance becomes a bound method
0x00B18C90>>

输出显示绑定和非绑定方法对象是两个不同的类型,实际的 PyMethod_Type C 代码实现 Objects/classobject.c 是一个单独的对象,这个对象会根据 im_self 这个字段是被设置或者为 NULL 来区分具体的类型。

同样的,调用方法对象时会判断 im_self 字段是否被设置,如果被设置就是绑定的方法,原来的函数(存在 im_func 字段中)就会被调用,并且会将方法第一个参数设置为实例对象。如果非绑定,所有实参就会原样传给函数。实际的 C 代码实现 instancemethod_call() 只是在原函数上面加了一些类型检查。

Static Methods and Class Methods

非数据描述符提供了一个简单的机制来将函数变成绑定的方法。我们再回忆下,函数得有个 __get__ 方法,这样当有属性访问的时候才能转换成方法。非数据描述符将 obj.f(*args) 转变成 f(obj, *args) ,调用 klass.f(*args) 即是 f(*args)

下面的表格对方法绑定以及两个最有用的变体做了总结

Python Descriptors 指南_第1张图片

当执行 c.f 或者 C.f 时,静态方法的属性访问方式为 object.__getattribute__(c, "f") 或者 object.__getattribute__(C, "f")。 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

静态方法适合做?
类方法适合做?

类方法的属性访问 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

你可能感兴趣的:(Python)