深入解析Python中的descriptor描述器的作用及用法

1033次阅读  |  发布于5年以前

一般来说,一个描述器是一个有"绑定行为"的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 get(), set(), 和 delete() 。有这些方法的对象叫做描述器。

默认对属性的访问控制是从对象的字典里面(dict)中获取(get), 设置(set)和删除(delete)它。举例来说, a.x 的查找顺序是, a.dict['x'] , 然后 type(a).dict['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)).如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)

描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和 super 的背后的实现机制。描述器在Python自身中广泛使用,以实现Python 2.2中引入的新式类。描述器简化了底层的C代码,并为Python的日常编程提供了一套灵活的新工具。

描述器协议


    descr.__get__(self, obj, type=None) --> value
    descr.__get__(self, obj, value) --> None
    descr.__delete__(self, obj) --> None

一个对象如果是一个描述器,被当做对象属性(很重要)时重写默认的查找行为。

如果一个对象同时定义了getset,它叫data descriptor。仅定义了get的描述器叫non-data descriptor。

data descriptor和non-data descriptor区别在于: 相对于实例的字典的优先级,如果实例字典有与描述器具同名的属性,如果描述器是data descriptor,优先使用data descriptor。如果是non-data descriptor,优先使用字典中的属性。


    class B(object):

      def __init__(self):
        self.name = 'mink'

      def __get__(self, obj, objtype=None):
        return self.name

    class A(object):
      name = B()

    a = A()
    print a.__dict__  # print {}
    print a.name    # print mink
    a.name = 'kk'    
    print a.__dict__  # print {'name': 'kk'}
    print a.name    # print kk

这里B是一个non-data descriptor所以当a.name = 'kk'的时候,a.dict里会有name属性, 接下来给它设置set


    def __set__(self, obj, value):
      self.name = value

     ... do something

    a = A()
    print a.__dict__  # print {}
    print a.name    # print mink
    a.name = 'kk'    
    print a.__dict__  # print {}
    print a.name    # print kk

因为data descriptor访问属性优先级比实例的字典高,所以a.dict是空的。

描述器的调用
描述器可以直接这么调用: d.get(obj)

然而更常见的情况是描述器在属性访问时被自动调用。举例来说, obj.d 会在 obj 的字典中找 d ,如果 d 定义了 get 方法,那么 d.get(obj) 会依据下面的优先规则被调用。

调用的细节取决于 obj 是一个类还是一个实例。另外,描述器只对于新式对象和新式类才起作用。继承于 object 的类叫做新式类。

对于对象来讲,方法 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

其中重要的几点:

注意:在Python 2.2中,如果 m 是一个描述器, super(B, obj).m() 只会调用方法 get() 。在Python 2.3中,非资料描述器(除非是个旧式类)也会被调用。 super_getattro() 的实现细节在: Objects/typeobject.c ,[del] 一个等价的Python实现在 Guido's Tutorial [/del] (译者注:原文此句已删除,保留供大家参考)。

以上展示了描述器的机理是在 object, type, 和 super 的 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), 方法(bound和unbound method), 静态方法和类方法都是基于描述器协议的。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8