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

一般来说,一个描述器是一个有“绑定行为”的对象属性(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,value) --> None
descr.__delete__(self,obj) --> None

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

如果一个对象同时定义了__get__和__set__,它叫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,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,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,objtype):
    print 'Retrieving',self.name
    return self.val

  def __set__(self,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),静态方法和类方法都是基于描述器协议的。

相关文章

方案一 代码 在Python中,可以使用wave模块来读取双通道(立...
简介 一个用python实现的科学计算,包括: 1、一个强大的N维...
使用爬虫利器 Playwright,轻松爬取抖查查数据 我们先分析登...
轻松爬取灰豚数据的抖音商品数据 调用两次登录接口实现模拟登...
成功绕过阿里无痕验证码,一键爬取飞瓜数据 飞瓜数据的登录接...
一文教你从零开始入门蝉妈妈数据爬取,成功逆向破解数据加密...