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进程通信之匿名管道实例讲解
Apr 11 Python
Python实现在线暴力破解邮箱账号密码功能示例【测试可用】
Sep 06 Python
MySQL适配器PyMySQL详解
Sep 20 Python
python实现二分查找算法
Sep 21 Python
python实现将多个文件分配到多个文件夹的方法
Jan 07 Python
Python GUI编程完整示例
Apr 04 Python
Python3 批量扫描端口的例子
Jul 25 Python
Pytorch.nn.conv2d 过程验证方式(单,多通道卷积过程)
Jan 03 Python
pandas实现excel中的数据透视表和Vlookup函数功能代码
Feb 14 Python
python实现Thrift服务端的方法
Apr 20 Python
python编写五子棋游戏
May 25 Python
浅谈python中的多态
Jun 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
PHP nl2br函数 将换行字符转成 &amp;lt;br&amp;gt;
2009/08/21 PHP
超级简单的php+mysql留言本源码
2009/11/11 PHP
php打乱数组二维数组多维数组的简单实例
2016/06/17 PHP
YUI 读码日记之 YAHOO.lang.is*
2008/03/22 Javascript
动态调用CSS文件的JS代码
2010/07/29 Javascript
js+jquery常用知识点汇总
2015/03/03 Javascript
深入理解JavaScript系列(21):S.O.L.I.D五大原则之接口隔离原则ISP详解
2015/03/05 Javascript
javascript实现画不相交的圆
2015/04/07 Javascript
jQuery实现MSN中文网滑动Tab菜单效果代码
2015/09/09 Javascript
JavaScript 随机验证码的生成实例代码
2016/09/22 Javascript
详解vue-cli3使用
2018/08/14 Javascript
vue实现form表单与table表格的数据关联功能示例
2019/01/29 Javascript
详解如何提升JSON.stringify()的性能
2019/06/12 Javascript
vue路由缓存的几种实现方式小结
2020/02/02 Javascript
js实现3D旋转效果
2020/08/18 Javascript
[01:38]DOTA2 2015国际邀请赛中国区预选赛 Showopen
2015/06/01 DOTA
Python获取文件ssdeep值的方法
2014/10/05 Python
pymongo为mongodb数据库添加索引的方法
2015/05/11 Python
Python爬取数据并实现可视化代码解析
2020/08/12 Python
python help函数实例用法
2020/12/06 Python
python 获取域名到期时间的方法步骤
2021/02/10 Python
编辑硕士自荐信范文
2013/11/27 职场文书
就业自荐书
2013/12/05 职场文书
汽车运用工程系毕业生自荐信
2013/12/27 职场文书
《得道多助,失道寡助》教学反思
2014/04/19 职场文书
民主生活会剖析材料
2014/09/30 职场文书
三严三实学习心得体会
2014/10/13 职场文书
证劵公司反洗钱宣传活动总结
2015/05/08 职场文书
五一晚会主持词
2015/07/01 职场文书
中学校园广播稿
2015/08/18 职场文书
2019暑期安全倡议书!
2019/06/27 职场文书
导游词之澳门玫瑰圣母堂
2019/12/03 职场文书
用Python制作灯光秀短视频的思路详解
2021/04/13 Python
详解Python如何批量采集京东商品数据流程
2022/01/22 Python
《仙剑客栈2》第一弹正式宣传片公开 年内发售
2022/04/07 其他游戏
前端传参数进行Mybatis调用mysql存储过程执行返回值详解
2022/08/14 MySQL