Python 中的Descriptor 详解
Python 中的成员变量
Descriptor 是什么?简而言之,Descriptor 是用来定制访问类或实例的成员的一种协议。额。。好吧,一句话是说不清楚的。下面先介绍一下Python 中成员变量的定义和使用。
我们知道,在Python 中定义类成员和C/C++ 相比得到的结果具有很大的差别。如下面的定义:
1 class Cclass
2 {
3 int I;
4 void func();
5 };
6
7 Cclass c; 而Python中类似的定义如下:
1 class Pclass(object):
2 i = 1
3 def func(self, x): return x
4
5 p = Pclass() 在上面的定义中,C++定义了一个类型,所有该类型的对象都包含有一个成员整数i和函数func;而Python则创建了一个名为Pclass、类型(__class__)为type(详情请参见MetaClass,Python中一切皆为对象,类型也不例外)的对象,然后再创建一个名为p、类型为Pclass的对象。如下所示:
1 In [71]: type(pclass)
2 Out[71]:
3 In [72]: p = pclass()
4 In [73]: type(p)
5 Out[73]: p 和Pclass 各自包含了一些成员,如下所示:
1 p.__class__ p.__init__ p.__sizeof__
2 p.__delattr__ p.__module__ p.__str__
3 p.__dict__ p.__new__ p.__subclasshook__
4 p.__doc__ p.__reduce__ p.__weakref__
5 p.__format__ p.__reduce_ex__ p.f
6 p.__getattribute__ p.__repr__ p.i
7 p.__hash__ p.__setattr__ 其中,带有双下划线的成员为特殊成员,或者可以称之为固定成员(和__slots__ 定义的成员类似),这些成员变量的值可以被改变,但不能被删除(del )。其中,__class__ 变量为对象所属的类型,__doc__ 为对象的文档字符串。有一个特殊成员值得注意:__dict__, 该字典中保存了对象的自定义变量。相信大家在初学Python 对于其中对象可以任意增加删除成员变量的能力感到惊讶,其实这个功能的玄机就在于__dict__ 成员中(注意type 的__dict__ 为dictproxy 类型):
1 In [10]: p.x = 2
2 In [11]: p.__dict__
3 Out[11]: {'x': 2} 通过上面的演示可以很清楚地看出:Python 将对象的自定义成员以键值对的形式保存到__dict__ 字典中,而前面提到的类型定义只是这种情况的语法糖而已,即上面的类型定义等价于以下形式的定义:
1 Class Pclass(object): pass
2 Pclass.i = 1
3 Pclass.f = lambda x: x 访问成员变量时,Python 也是从__dict__ 字典中取出变量名对应的值,如下形式的两种访问形式是等价的——在Descriptor 被引入之前:
1 p.i
2 p.__dict__['i'] Descriptor 的引入即将改变上面的规则,且看下文分解。
定义:DescriptorProtocol
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 class MyDescriptor(object):
2 def __init__(self, x):
3 self.x = x
4 def __get__(self, instance, owner):
5 print 'get from descriptor'
6 return self.x
7 def __set__(self, instance, value):
8 print 'set from descriptor'
9 self.x = value
10 def __delete__(self, instance)
11 print 'del from descriptor, the val is', self.x
12
13 class C(object):
14 d = MyDescriptor('hello')
15
16 >> C.d
17 get from descriptor
18
19 >> c = C()
20 >> c.d
21 get from descriptor
22
23 >> c.d = 1
24 set from descriptor
25
26 >> del c.d
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 为一个OverridingDescriptor ,则返回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 为一个OverridingDescriptor ,因此执行d.__get__(c,C) 。
前面介绍了Descriptor 的一些细节,那么Descriptor 的作用是什么呢?在Python 中,Descriptor 主要用来实现一些Python 本身的功能,如类方法调用、staticmethod 和Property 等。下面将对这些使用Descriptor 进行类方法调用的实现进行介绍。
Bound &Unbound Method
在python 中,函数是第一级的对象,即其本质与其它对象相同,差别在于函数对象是callable 对象,即对于函数对象f ,可以用语法f() 来调用函数。上面提到的对象成员访问规则,对于函数来说是完全一样的。Python 在实现成员函数调用时obj.f() 时,会执行一下两个步骤:
根据对象成员访问规则获取函数对象;
用函数对象执行函数调用;
为了验证上述过程,我们可以执行以下代码:
1 Class C(object):
2 def f(self):
3 pass
4 >> fun = C.f
5 Unbound Method
6 >> fun()
7 >> c = C()
8 >> fun = c.f
9 Bound Method
10 >> fun() 我们可以看到C.f 和c.f 返回了instancemethod 类型的对象,这两个对象也是可调用的,但是却不是我们本以为的func 对象。那么instancemethod 对象和func 对象之间具有什么关联呢?
func 类型:func 类型为Python 中原始的函数对象类型,即deff(): pass 将定义一个func 类型的对象f ;
instancemethod :func 的一个wrapper ,如果类方法没有绑定到对象,则该instancemethod 为一个UnboundMethod ,对UnboundMethod 的调用将导致TypeError 错误;如果类方法绑定到了对象,则该instancemethod 为一个BoundMethod ,对BoundMethod 的调用不许要指定self 参数的值。
如果查看UnboundMethod 对象和BoundMethod 对象的成员,我们可以发现它们都包含了一下三个成员:im_func 、im_self 和im_class 。其中im_func 为所封装的func 对象,im_self 则为所绑定对象的值,而im_class 则为定义该函数的类对象。由此我们可以知道,Python 会根据不同的情况返回函数的不同wrapper ,当通过类对象访问函数时,返回的是名为UnboundMethod 对象的Wrapper ,而通过类实例访问函数是,返回的则是绑定了该实例的名为BoundMethod 对象的Wrapper 。
现在是Descriptor 大显身手的时候了。
Python 中将func 定义为一个OverridingDescriptor ,在其__get__ 方法中构造一个instancemethod 对象,并根据被访问函数被访问的情况设置im_func 、im_self 和im_class 成员。在instancemethod 实例被调用时,则根据im_func 和im_self 来完成真正的函数调用。演示这一过程的代码如下:
1 Class instancemethod(object):
2 def __call__(self, *args):
3 if self.im_self == None:
4 raise 'unbound error'
5 return self.im_func(self.im_self, *args)
6 def __init__(self, im_self, im_func, im_class):
7 self.im_self = im_self
8 self.im_func = im_func
9 self.im_class = im_class
10
11 class func(object):
12 ...
13 def __get__(self, instance, owner):
14 return instancemethod(instance, self, owner)
15 def __set__(self, instance, value):
16 pass
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 才是被关注的重点,性能神马的就别提了。。
运维网声明
1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网 享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com