免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 3761 | 回复: 0
打印 上一主题 下一主题

Python中的Descriptor 详解 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2010-11-08 10:22 |只看该作者 |倒序浏览
Python中的成员变量
    Descriptor是什么?简而言之,Descriptor是用来定制访问类或实例的成员的一种协议。额。。好吧,一句话是说不清楚的。下面先介绍一下Python中成员变量的定义和使用。
    我们知道,在Python中定义类成员和C/C++相比得到的结果具有很大的差别。如下面的定义:
  1. 1 class Cclass
  2. 2 {
  3. 3     int I;
  4. 4     void func();
  5. 5 };
  6. 6
  7. 7 Cclass c;
复制代码
而Python中类似的定义如下:
  1. 1 class Pclass(object):
  2. 2     i = 1
  3. 3     def func(self, x): return x
  4. 4
  5. 5 p = Pclass()
复制代码
在上面的定义中,C++定义了一个类型,所有该类型的对象都包含有一个成员整数i和函数func;而Python则创建了一个名为Pclass、类型(__class__)为type(详情请参见MetaClass,Python中一切皆为对象,类型也不例外)的对象,然后再创建一个名为p、类型为Pclass的对象。如下所示:
  1. 1 In [71]: type(pclass)
  2. 2 Out[71]: <type 'type'>
  3. 3 In [72]: p = pclass()
  4. 4 In [73]: type(p)
  5. 5 Out[73]: <class '__main__.pclass'>
复制代码
p和Pclass各自包含了一些成员,如下所示:
  1. 1 p.__class__ p.__init__ p.__sizeof__
  2. 2 p.__delattr__ p.__module__ p.__str__
  3. 3 p.__dict__ p.__new__ p.__subclasshook__
  4. 4 p.__doc__ p.__reduce__ p.__weakref__
  5. 5 p.__format__ p.__reduce_ex__ p.f
  6. 6 p.__getattribute__ p.__repr__ p.i
  7. 7 p.__hash__ p.__setattr__
复制代码
其中,带有双下划线的成员为特殊成员,或者可以称之为固定成员(和__slots__定义的成员类似),这些成员变量的值可以被改变,但不能被删除(del)。其中,__class__变量为对象所属的类型,__doc__为对象的文档字符串。有一个特殊成员值得注意:__dict__,该字典中保存了对象的自定义变量。相信大家在初学Python对于其中对象可以任意增加删除成员变量的能力感到惊讶,其实这个功能的玄机就在于__dict__成员中(注意type的__dict__为dictproxy类型):
  1. 1 In [10]: p.x = 2
  2. 2 In [11]: p.__dict__
  3. 3 Out[11]: {'x': 2}
复制代码
通过上面的演示可以很清楚地看出:Python将对象的自定义成员以键值对的形式保存到__dict__字典中,而前面提到的类型定义只是这种情况的语法糖而已,即上面的类型定义等价于以下形式的定义:
  1. 1 Class Pclass(object): pass
  2. 2 Pclass.i = 1
  3. 3 Pclass.f = lambda x: x
复制代码
访问成员变量时,Python也是从__dict__字典中取出变量名对应的值,如下形式的两种访问形式是等价的——在Descriptor被引入之前:
  1. 1 p.i
  2. 2 p.__dict__['i']
复制代码
Descriptor的引入即将改变上面的规则,且看下文分解。
定义:Descriptor Protocol
    Descriptor如何改变对象成员的访问规则呢?根据计算机理论中“绝大多数软件问题都可以用增加一个中间层的方式解决”这一名言,我们需要为对象访问提供一个中间层,而非直接访问所需的对象。实现这一中间层的方式是定义Descriptor协议。Descriptor的定义很简单,如果一个类包含以下三个方法(之一),则可以称之为一个Descriptor:

object.__get__(self, instance, owner)

成员被访问时调用,instance为成员所属的对象、owner为instance所属的类型

object.__set__(self, instance, value)

成员被赋值时调用

object.__delete__(self, instance)

成员被删除时调用

    如果我们需要改变一个对象在其它对象中的访问规则,需要将其定义成Descriptor,之后在对该成员进行访问时将调用该Descriptor的相应函数。下面是一个使用Descriptor改变访问规则的例子:
  1. 1 class MyDescriptor(object):
  2. 2     def __init__(self, x):
  3. 3         self.x = x
  4. 4     def __get__(self, instance, owner):
  5. 5         print 'get from descriptor'
  6. 6         return self.x
  7. 7     def __set__(self, instance, value):
  8. 8         print 'set from descriptor'
  9. 9         self.x = value
  10. 10     def __delete__(self, instance)
  11. 11         print 'del from descriptor, the val is', self.x
  12. 12
  13. 13 class C(object):
  14. 14     d = MyDescriptor('hello')
  15. 15
  16. 16 >> C.d
  17. 17 get from descriptor
  18. 18
  19. 19 >> c = C()
  20. 20 >> c.d
  21. 21 get from descriptor
  22. 22
  23. 23 >> c.d = 1
  24. 24 set from descriptor
  25. 25
  26. 26 >> del c.d
  27. 27 del from descriptor, the val is 1
复制代码
从例子中可以看出:当我们对对象成员进行引用(Reference)、赋值(Assign)和删除(Dereference)操作时,如果对象成员为一个Descriptor,则这些操作将执行该Descriptor对象的相应成员函数。以上约定即为Descriptor协议。

obj.name背后的魔法
    引入了Descriptor之后,Python对于对象成员访问的规则是怎样的呢?在回答这一问题之前,需要对Descriptor进行简单的划分:

Overriding或Data:对象同时提供了__get__和__set__方法

Nonoverriding或Non-Data:对象仅提供了__get__方法

    (__del__方法表示自己被忽略了,很伤心~)

    下面是从一个类对象中访问其成员(如C.name)的规则:

如果“name”在C.__dict__能找到,C.name将访问C.__dict__['name'],假设为v。如果v是一个Descriptor,则返回type(v).__get__(v, None, C),否则直接返回v;

如果“name”不在C.__dict__中,则向上查找C的父类,根据MRO(Method Resolution Order)对C的父类重复第一步;

还是没有找到“name”,抛出AttributeError异常。

    从一个类实例对象中访问其成员(如x.name,type(x)为C)要稍微复杂一些:

如果“name”能在C(或C的父类)中找到,且其值v为一个Overriding Descriptor,则返回type(v).__get__(v, x, C)的值;

否则,如果“name”能在x.__dict__中找到,则返回x.__dict__['name']的值;

如果“name”仍未找到,则执行类对象成员的查找规则;

如果C定义了__getattr__函数,则调用该函数;否则抛出AttributeError异常。

    成员赋值的查找规则与访问规则类似,但还是有一点区别:对类成员执行赋值操作时将直接设置C.__dict__中的值,而不会调用Descriptor的__set__函数。

    以上面的代码为例,当访问C.d时,Python将在C.__dict__中找到d,并且发现d是一个Descriptor,因此将调用d.__get__(None, C);当访问c.d时,Python首先查找C,并且在其中发现d的定义,且d为一个Overriding Descriptor,因此执行d.__get__(c, C)。

    前面介绍了Descriptor的一些细节,那么Descriptor的作用是什么呢?在Python中,Descriptor主要用来实现一些Python本身的功能,如类方法调用、staticmethod和Property等。下面将对这些使用Descriptor进行类方法调用的实现进行介绍。

Bound & Unbound Method
    在python中,函数是第一级的对象,即其本质与其它对象相同,差别在于函数对象是callable对象,即对于函数对象f,可以用语法f()来调用函数。上面提到的对象成员访问规则,对于函数来说是完全一样的。Python在实现成员函数调用时obj.f()时,会执行一下两个步骤:

根据对象成员访问规则获取函数对象;

用函数对象执行函数调用;

    为了验证上述过程,我们可以执行以下代码:
  1. 1 Class C(object):
  2. 2     def f(self):
  3. 3         pass
  4. 4 >> fun = C.f
  5. 5 Unbound Method
  6. 6 >> fun()
  7. 7 >> c = C()
  8. 8 >> fun = c.f
  9. 9 Bound Method
  10. 10 >> fun()
复制代码
  1. 我们可以看到C.f和c.f返回了instancemethod类型的对象,这两个对象也是可调用的,但是却不是我们本以为的func对象。那么instancemethod对象和func对象之间具有什么关联呢?

  2. func类型:func类型为Python中原始的函数对象类型,即def f(): pass将定义一个func类型的对象f;

  3. instancemethod:func的一个wrapper,如果类方法没有绑定到对象,则该instancemethod为一个Unbound Method,对Unbound Method的调用将导致TypeError错误;如果类方法绑定到了对象,则该instancemethod为一个Bound Method,对Bound Method的调用不许要指定self参数的值。

  4.     如果查看Unbound Method对象和Bound Method对象的成员,我们可以发现它们都包含了一下三个成员:im_func、im_self和im_class。其中im_func为所封装的func对象,im_self则为所绑定对象的值,而im_class则为定义该函数的类对象。由此我们可以知道,Python会根据不同的情况返回函数的不同wrapper,当通过类对象访问函数时,返回的是名为Unbound Method对象的Wrapper,而通过类实例访问函数是,返回的则是绑定了该实例的名为Bound Method对象的Wrapper。

  5.     现在是Descriptor大显身手的时候了。

  6.     Python中将func定义为一个Overriding Descriptor,在其__get__方法中构造一个instancemethod对象,并根据被访问函数被访问的情况设置im_func、im_self和im_class成员。在instancemethod实例被调用时,则根据im_func和im_self来完成真正的函数调用。演示这一过程的代码如下:
复制代码
  1. 1 Class instancemethod(object):
  2. 2     def __call__(self, *args):
  3. 3         if self.im_self == None:
  4. 4         raise 'unbound error'
  5. 5         return self.im_func(self.im_self, *args)
  6. 6     def __init__(self, im_self, im_func, im_class):
  7. 7         self.im_self = im_self
  8. 8         self.im_func = im_func
  9. 9         self.im_class = im_class
  10. 10
  11. 11 class func(object):
  12. 12     ...
  13. 13     def __get__(self, instance, owner):
  14. 14         return instancemethod(instance, self, owner)
  15. 15     def __set__(self, instance, value):
  16. 16         pass
  17. 17     ...
复制代码
小结
    Descriptor是访问对象成员时的一个中间层,为我们提供了自定义对象成员访问的方式。通过对Descriptor的探索,对原来的一些看似神秘的概念顿时有种豁然开朗的感觉:

类方法调用:编译器并没有为其提供专门的语法规则,而是使用Descriptor返回instancemethod来封装func,从而实现类似obj.func()的调用方式;

staticmethod:decorator将创建一个StaticMethod并在其中保存func对象,StaticMethod是一个Descriptor,其__get__函数中返回前面所保存的func对象;

Property:创建一个Property对象,在其__get__、__set__和__delete__方法中分别执行构造对象是传入的fget、fset、和fdel函数。现在知道为什么Property只提供这三个函数作为参数么。。

    最后一个问题是,Python引入Descriptor之后的性能会不会有影响?性能影响是必须的:每次访问成员时的查找规则,之后再调用Descriptor的__get__函数,如果是方法调用的话之后才是执行真正的函数调用。每次访问对象成员时都要经历以上过程,对Python的性能应该会有较大的影响。但是,在Python的世界,貌似Pythonic才是被关注的重点,性能神马的就别提了。。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP