Python黑魔法Descriptor描述符的实例解析


Posted in Python onJune 02, 2016

在Python中,访问一个属性的优先级顺序按照如下顺序:
1:类属性
2:数据描述符
3:实例属性
4:非数据描述符
5:__getattr__()方法  这个方法的完整定义如下所示:

def __getattr(self,attr) :#attr是self的一个属性名 
 pass;

先来阐述下什么叫数据描述符。
数据描述符是指实现了__get__,__set__,__del__方法的类属性(由于Python中,一切皆是对象,所以你不妨把所有的属性也看成是对象)
PS:个人觉得这里最好把数据描述符等效于定义了__get__,__set__,__del__三个方法的接口。

__get__,__set__,__del__
阐述下这三个方法:
 __get__的标准定义是__get__(self,obj,type=None),它非常接近于JavaBean的get
第一个函数是调用它的实例,obj是指去访问属性所在的方法,最后一个type是一个可选参数,通常为None(这个有待于进一步的研究)
例如给定类X和实例x,调用x.foo,等效于调用:

type(x).__dict__['foo'].__get__(x,type(x))

调用X.foo,等效于调用:

type(x).__dict__['foo'].__get__(None,type(x))

 
第二个函数__set__的标准定义是__set__(self,obj,val),它非常接近于JavaBean的set方法,其中最后一个参数是要赋予的值
第三个函数__del__的标准定义是__del__(self,obj),它非常接近Java中Object的Finailize()方法,指Python在回收这个垃圾对象时所调用到的析构函数,只是这个函数永远不会抛出异常。因为这个对象已经没有引用指向它,抛出异常没有任何意义。
 
优先级
接下来,我们来一一比较这些优先级.
首先来看类属性

class A(object): 
 foo=1.3; 
  
print str(A.__dict__);

输出: 

{'__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', 

'foo': 1.3, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}

从上图可以看出foo属性在类的__dict__属性里,所以这里用A.foo可以直接找到。这里我们先跨过数据描述符,直接来看实例属性.

class A(object): 
 foo=1.3; 
 
a=A(); 
print a.foo; 
a.foo=15; 
print a.foo;

这里a.foo先输出1.3后输出15,不是说类属性的优先级比实例属性的优先级高吗?按理a.foo应该不变才对?其实,这里只是一个假象,真正的原因在于这里将a.foo这个引用对象,不妨将其理解为可以指向任意数据类型的指针,指向了15这个int对象。
不信,可以继续看:

class A(object): 
 foo=1.3; 
 
a=A(); 
print a.foo; 
a.foo=15; 
print a.foo; 
del a.foo; 
print a.foo;

 这次在输出1.3,15后最后一次又一次的输出了1.3,原因在于a.foo最后一次又按照优先级顺序直接找到了类属性A.foo

描述器与对象属性
OOP的理论中,类的成员变量包括属性和方法。那么在Python里什么是属性?修改上面的PythonSite类如下:

class PythonSite(object):

 webframework = WebFramework()

 version = 0.01

 def __init__(self, site):
 self.site = site

这里增加了一个version的类属性,以及一个实例属性site。分别查看一下类和实例对象的属性:

In [1]: pysite = PythonSite('ghost')

In [2]: vars(PythonSite).items()
Out[2]:
[('__module__', '__main__'),
 ('version', 0.01),
 ('__dict__', <attribute '__dict__' of 'PythonSite' objects>),
 ('webframework', <__main__.WebFramework at 0x10d55be90>),
 ('__weakref__', <attribute '__weakref__' of 'PythonSite' objects>),
 ('__doc__', None),
 ('__init__', <function __main__.__init__>)]

In [3]: vars(pysite)
Out[3]: {'site': 'ghost'}
In [4]: PythonSite.__dict__
Out[4]:
<dictproxy {'__dict__': <attribute '__dict__' of 'PythonSite' objects>,
 '__doc__': None,
 '__init__': <function __main__.__init__>,
 '__module__': '__main__',
 '__weakref__': <attribute '__weakref__' of 'PythonSite' objects>,
 'version': 0.01,
 'webframework': <__main__.WebFramework at 0x10d55be90>}>

vars方法用于查看对象的属性,等价于对象的__dict__内容。从上面的显示结果,可以看到类PythonSite和实例pysite的属性差别在于前者有 webframework,version两个属性,以及 __init__方法,后者仅有一个site属性。

类与实例的属性
类属性可以使用对象和类访问,多个实例对象共享一个类变量。但是只有类才能修改。

In [6]: pysite1 = PythonSite('ghost')

In [7]: pysite2 = PythonSite('admin')

In [8]: PythonSite.version
Out[8]: 0.01

In [9]: pysite1.version
Out[9]: 0.01

In [10]: pysite2.version
Out[10]: 0.01

In [11]: pysite1.version is pysite2.version
Out[11]: True

In [12]: pysite1.version = 'pysite1'

In [13]: vars(pysite1)
Out[13]: {'site': 'ghost', 'version': 'pysite1'}

In [14]: vars(pysite2)
Out[14]: {'site': 'admin'}

In [15]: PythonSite.version = 0.02

In [16]: pysite1.version
Out[16]: 'pysite1'

In [17]: pysite2.version
Out[17]: 0.02

正如上面的代码显示,两个实例对象都可以访问version类属性,并且是同一个类属性。当pysite1修改了version,实际上是给自己添加了一个version属性。类属性并没有被改变。当PythonSite改变了version属性的时候,pysite2的该属性也对应被改变。

属性访问的原理与描述器
知道了属性访问的结果。这个结果都是基于Python的描述器实现的。通常,类或者实例通过.操作符访问属性。例如pysite1.site和pysite1.version的访问。先访问对象的__dict__,如果没有再访问类(或父类,元类除外)的__dict__。如果最后这个__dict__的对象是一个描述器,则会调用描述器的__get__方法。

In [21]: pysite1.site
Out[21]: 'ghost'

In [22]: pysite1.__dict__['site']
Out[22]: 'ghost'

In [23]: pysite2.version
Out[23]: 0.02

In [24]: pysite2.__dict__['version']
---------------------------------------------------------------------------
KeyError     Traceback (most recent call last)
<ipython-input-24-73ef6aeba259> in <module>()
----> 1 pysite2.__dict__['version']

KeyError: 'version'

In [25]: type(pysite2).__dict__['version']
Out[25]: 0.02

In [32]: type(pysite1).__dict__['webframework']
Out[32]: <__main__.WebFramework at 0x103426e90>

In [38]: type(pysite1).__dict__['webframework'].__get__(None, PythonSite)
Out[38]: 'Flask'

实例方法,类方法,静态方法与描述器
调用描述器的时候,实际上会调用object.__getattribute__()。这取决于调用描述其器的是对象还是类,如果是对象obj.x,则会调用type(obj).__dict__['x'].__get__(obj, type(obj))。如果是类,class.x, 则会调用type(class).__dict__['x'].__get__(None, type(class)。

这样说还是比较抽象,下面来分析Python的方法,静态方法和类方法。把PythonSite重构一下:

class PythonSite(object):
 webframework = WebFramework()

 version = 0.01

 def __init__(self, site):
 self.site = site

 def get_site(self):
 return self.site

 @classmethod
 def get_version(cls):
 return cls.version

 @staticmethod
 def find_version():
 return PythonSite.version

类方法,@classmethod装饰器
先看类方法,类方法使用@classmethod装饰器定义。经过该装饰器的方法是一个描述器。类和实例都可以调用类方法:

In [1]: ps = PythonSite('ghost')

In [2]: ps.get_version
Out[2]: <bound method type.get_version of <class '__main__.PythonSite'>>

In [3]: ps.get_version()
Out[3]: 0.01

In [4]: PythonSite.get_version
Out[4]: <bound method type.get_version of <class '__main__.PythonSite'>>

In [5]: PythonSite.get_version()
Out[5]: 0.01

get_version 是一个bound方法。下面再看下ps.get_version这个调用,会先查找它·的__dict__是否有get_version这个属性,如果没有,则查找其类。

In [6]: vars(ps)
Out[6]: {'site': 'ghost'}

In [7]: type(ps).__dict__['get_version']
Out[7]: <classmethod at 0x108952e18>

In [8]: type(ps).__dict__['get_version'].__get__(ps, type(ps))
Out[8]: <bound method type.get_version of <class '__main__.PythonSite'>>

In [9]: type(ps).__dict__['get_version'].__get__(ps, type(ps)) == ps.get_version
Out[9]: True

并且vars(ps)中,__dict__并没有get_version这个属性,依据描述器协议,将会调用type(ps).__dict__['get_version']描述器的__get__方法,因为ps是实例,因此object.__getattribute__()会这样调用__get__(obj, type(obj))。

现在再看类方法的调用:

In [10]: PythonSite.__dict__['get_version']
Out[10]: <classmethod at 0x108952e18>

In [11]: PythonSite.__dict__['get_version'].__get__(None, PythonSite)
Out[11]: <bound method type.get_version of <class '__main__.PythonSite'>>

In [12]: PythonSite.__dict__['get_version'].__get__(None, PythonSite) == PythonSite.get_version
Out[12]: True

因为这次调用get_version的是一个类对象,而不是实例对象,因此object.__getattribute__()会这样调用__get__(None, Class)。

静态方法,@staticmethod
实例和类也可以调用静态方法:

In [13]: ps.find_version
Out[13]: <function __main__.find_version>

In [14]: ps.find_version()
Out[14]: 0.01

In [15]: vars(ps)
Out[15]: {'site': 'ghost'}

In [16]: type(ps).__dict__['find_version']
Out[16]: <staticmethod at 0x108952d70>

In [17]: type(ps).__dict__['find_version'].__get__(ps, type(ps))
Out[17]: <function __main__.find_version>

In [18]: type(ps).__dict__['find_version'].__get__(ps, type(ps)) == ps.find_version
Out[18]: True

In [19]: PythonSite.find_version()
Out[19]: 0.01

In [20]: PythonSite.find_version
Out[20]: <function __main__.find_version>

In [21]: type(ps).__dict__['find_version'].__get__(None, type(ps))
Out[21]: <function __main__.find_version>

In [22]: type(ps).__dict__['find_version'].__get__(None, type(ps)) == PythonSite.find_version
Out[22]: True

和类方法差别不大,他们的主要差别是在类方法内部的时候,类方法可以有cls的类引用,静态访问则没有,如果静态方法想使用类变量,只能硬编码类名。

实例方法
实例方法最为复杂,是专门属于实例的,使用类调用的时候,会是一个unbound方法。

In [2]: ps.get_site
Out[2]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>>

In [3]: ps.get_site()
Out[3]: 'ghost'

In [4]: type(ps).__dict__['get_site']
Out[4]: <function __main__.get_site>

In [5]: type(ps).__dict__['get_site'].__get__(ps, type(ps))
Out[5]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>>

In [6]: type(ps).__dict__['get_site'].__get__(ps, type(ps)) == ps.get_site
Out[6]: True

一切工作正常,实例方法也是类的一个属性,但是对于类,描述器使其变成了unbound方法:

In [7]: PythonSite.get_site
Out[7]: <unbound method PythonSite.get_site>

In [8]: PythonSite.get_site()
---------------------------------------------------------------------------
TypeError     Traceback (most recent call last)
<ipython-input-8-99c7d7607137> in <module>()
----> 1 PythonSite.get_site()

TypeError: unbound method get_site() must be called with PythonSite instance as first argument (got nothing instead)

In [9]: PythonSite.get_site(ps)
Out[9]: 'ghost'

In [10]: PythonSite.__dict__['get_site']
Out[10]: <function __main__.get_site>

In [11]: PythonSite.__dict__['get_site'].__get__(None, PythonSite)
Out[11]: <unbound method PythonSite.get_site>

In [12]: PythonSite.__dict__['get_site'].__get__(None, PythonSite) == PythonSite.get_site
Out[12]: True

In [14]: PythonSite.__dict__['get_site'].__get__(ps, PythonSite)
Out[14]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>>

In [15]: PythonSite.__dict__['get_site'].__get__(ps, PythonSite)()
Out[15]: 'ghost'

由此可见,类不能直接调用实例方法,除非在描述器手动绑定一个类实例。因为使用类对象调用描述器的时候,__get__的第一个参数是None,想要成功调用,需要把这个参数替换为实例ps,这个过程就是对方法的bound过程。

实例 
按照之前的定义,一个实现了__get__,__set__,__del__的类都统称为数据描述符。我们来看下一个简单的例子.

class simpleDescriptor(object): 
 def __get__(self,obj,type=None) : 
 pass; 
 def __set__(self,obj,val): 
 pass; 
 def __del__(self,obj): 
 pass 
 
class A(object): 
 foo=simpleDescriptor(); 
print str(A.__dict__); 
print A.foo; 
a=A(); 
print a.foo; 
a.foo=13; 
print a.foo;

 
这里get,set,del方法体内容都略过,虽然简单,但也不失为一个数据描述符。让我们来看下它的输出:

{'__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', 
'foo': <__main__.simpleDescriptor object at 0x00C46930>, 
'__weakref__': <attribute '__weakref__' of 'A' objects>, 
'__doc__': None} 
None 
None 
None

 
从上图可以看出,尽管我们对a.foo赋值了,但其依然为None,原因就在于__get__方法什么都不返回。
为了更进一步的加深对数据描述符的理解,我们简单的作下改造.

class simpleDescriptor(object): 
 def __init__(self): 
 self.result=None; 
 def __get__(self,obj,type=None) : 
 return self.result-10; 
 def __set__(self,obj,val): 
 self.result=val+3; 
 print self.result; 
 def __del__(self,obj): 
 pass 
 
class A(object): 
 foo=simpleDescriptor(); 
a=A(); 
a.foo=13; 
print a.foo;

打印的输出结果为:

16
 6

第一个16为我们在对a.foo赋值的时候,人为的将13加上3后作为foo的值,第二个6是我们在返回a.foo之前人为的将它减去了10。
所以我们可以猜测,常规的Python类在定义get,set方法的时候,如果无特殊需求,直接给对应的属性赋值或直接返回该属性值。如果自己定义类,并且继承object类的话,这几个方法都不用定义。
下面我们来看下实例属性和非数据描述符。

class B(object): 
 foo=1.3; 
b=B(); 
print b.__dict__ 
#print b.bar; 
b.bar=13; 
print b.__dict__ 
print b.bar;

输出结果为:

{}
{'bar': 13}
13

可见这里在实例b.__dict__里找到了bar属性,所以这次可以获取13了
那么什么是非数据描述符呢?简单的说,就是没有实现get,set,del三个方法的所有类
让我们任意看一个函数的描述:

def hello(): 
 pass 
 
print dir(hello)

 
输出:  

['__call__', '__class__', '__delattr__', '__dict__', 
'__doc__', 
'__get__', 
'__getattribute__', 
'__hash__', '__init__', '__module__', '__name__', 
 '__new__', '__reduce__', 
'__reduce_ex__', '__repr__', 
 '__setattr__', '__str__', 'func_closure', 
'func_code', 
'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

 
从上面可以看出所有的函数都有get方法,但都没有set和del方法,所以所有的类成员函数都是非数据描述符。
看一个简单的例子:

class simpleDescriptor(object): 
 def __get__(self,obj,type=None) : 
 return 'get',self,obj,type; 
class D(object): 
 foo=simpleDescriptor(); 
d=D(); 
print d.foo; 
d.foo=15; 
print d.foo;

输出:

('get', <__main__.simpleDescriptor object at 0x00C46870>, 

<__main__.D object at 0x00C46890>, <class '__main__.D'>) 
15

 
可以看出实例属性掩盖了非数据描述符。
最后看下__getatrr__方法。它的标准定义是:__getattr__(self,attr),其中attr是属性名
让我们来看一个简单的例子:

class D(object): 
 def __getattr__(self,attr): 
 return attr; 
 #return self.attr; 
  
d=D(); 
print d.foo,type(d.foo); 
d.foo=15; 
print d.foo;

 输出:

foo <type 'str'>
 15

 可以看的出来Python在实在找不到方法的时候,就会求助于__getattr__方法。
 注意这里要避免无意识的递归,稍微改动下:

class D(object): 
 def __getattr__(self,attr): 
 #return attr; 
 return self.attr; 
  
d=D(); 
print d.foo,type(d.foo); 
d.foo=15; 
print d.foo;

 
这次会直接抛出堆栈溢出的异常,就像下面这样:

RuntimeError: maximum recursion depth exceeded
Python 相关文章推荐
Python正则表达式介绍
Aug 06 Python
Python中的index()方法使用教程
May 18 Python
Python中函数的参数传递与可变长参数介绍
Jun 30 Python
对Python中list的倒序索引和切片实例讲解
Nov 15 Python
对python列表里的字典元素去重方法详解
Jan 21 Python
基于PyQt4和PySide实现输入对话框效果
Feb 27 Python
Django对数据库进行添加与更新的例子
Jul 12 Python
在python3中实现查找数组中最接近与某值的元素操作
Feb 29 Python
pandas分组聚合详解
Apr 10 Python
mac安装python3后使用pip和pip3的区别说明
Sep 01 Python
python 动态绘制爱心的示例
Sep 27 Python
PyCharm Ctrl+Shift+F 失灵的简单有效解决操作
Jan 15 Python
深入理解Python变量与常量
Jun 02 #Python
Python中的Descriptor描述符学习教程
Jun 02 #Python
从源码解析Python的Flask框架中request对象的用法
Jun 02 #Python
Python搭建APNS苹果推送通知推送服务的相关模块使用指南
Jun 02 #Python
Python的Django框架中使用SQLAlchemy操作数据库的教程
Jun 02 #Python
实例解析Python中的__new__特殊方法
Jun 02 #Python
详解Python中的__new__、__init__、__call__三个特殊方法
Jun 02 #Python
You might like
10个超级有用的PHP代码片段果断收藏
2015/09/23 PHP
多广告投放代码 推荐
2006/11/13 Javascript
JavaScript 乱码问题
2009/08/06 Javascript
WEB页子窗口(showModalDialog和showModelessDialog)使用说明
2009/10/25 Javascript
js实现GridView单选效果自动设置交替行、选中行、鼠标移动行背景色
2010/05/27 Javascript
IE6弹出“已终止操作”的解决办法
2010/11/27 Javascript
jquery对ajax的支持介绍
2013/12/10 Javascript
Jquery uploadify图片上传插件无法上传的解决方法
2013/12/16 Javascript
jquery插件冲突(jquery.noconflict)解决方法分享
2014/03/20 Javascript
z-blog SyntaxHighlighter 长代码无法换行解决办法(基于jquery)
2015/11/18 Javascript
浅谈String.valueOf()方法的使用
2016/06/06 Javascript
基于JS实现导航条之调用网页助手小精灵的方法
2016/06/17 Javascript
JS创建对象的写法示例
2016/11/04 Javascript
详解React-Todos入门例子
2016/11/08 Javascript
详解Vue2 无限级分类(添加,删除,修改)
2017/03/07 Javascript
分享ES6的7个实用技巧
2018/01/18 Javascript
vue router+vuex实现首页登录验证判断逻辑
2018/05/17 Javascript
JS+CSS3实现的简易钟表效果示例
2019/04/13 Javascript
24个解决实际问题的ES6代码片段(小结)
2020/02/02 Javascript
ng-alain的sf如何自定义部件的流程
2020/06/12 Javascript
微信小程序实现拨打电话功能的示例代码
2020/06/28 Javascript
[01:32:10]NAVI vs VG Supermajor 败者组 BO3 第一场 6.5
2018/06/06 DOTA
跟老齐学Python之赋值,简单也不简单
2014/09/24 Python
python中异常报错处理方法汇总
2016/11/20 Python
Python使用itertools模块实现排列组合功能示例
2018/07/02 Python
如何实现更换Jupyter Notebook内核Python版本
2020/05/18 Python
HTML5 UTF-8 中文乱码的解决方法
2013/11/18 HTML / CSS
静态成员和非静态成员的区别
2012/05/12 面试题
团工委书记自荐书范文
2013/12/17 职场文书
有创意的广告词
2014/03/18 职场文书
摄影展策划方案
2014/06/02 职场文书
机关作风建设剖析材料
2014/10/11 职场文书
法制教育讲座心得体会
2016/01/14 职场文书
python-for x in range的用法(注意要点、细节)
2021/05/10 Python
Java实战之课程信息管理系统的实现
2022/04/01 Java/Android
MySql分区类型及创建分区的方法
2022/04/13 MySQL