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自动重试HTTP连接装饰器
Apr 28 Python
Python入门之后再看点什么好?
Mar 05 Python
基于python 二维数组及画图的实例详解
Apr 03 Python
Python装饰器原理与用法分析
Apr 30 Python
Python自定义函数实现求两个数最大公约数、最小公倍数示例
May 21 Python
详解Python最长公共子串和最长公共子序列的实现
Jul 07 Python
详解Python 调用C# dll库最简方法
Jun 20 Python
通过实例简单了解Python中yield的作用
Dec 11 Python
Python 日期的转换及计算的具体使用详解
Jan 16 Python
浅谈Python协程
Jun 17 Python
MoviePy简介及Python视频剪辑自动化
Dec 18 Python
Python+Xlwings 删除Excel的行和列
Dec 19 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
C#使用PHP服务端的Web Service通信实例
2014/04/08 PHP
今天你说520了吗?不仅有php表白书还有java表白神器
2016/05/20 PHP
深入理解 PHP7 中全新的 zval 容器和引用计数机制
2018/10/15 PHP
php设计模式之迭代器模式实例分析【星际争霸游戏案例】
2020/04/07 PHP
关于用Jquery的height()、width()计算动态插入的IMG标签的宽高的问题
2010/12/08 Javascript
推荐40个简单的 jQuery 导航插件和教程(下篇)
2012/09/14 Javascript
javascript控制在光标位置插入文字适合表情的插入
2014/06/09 Javascript
node.js中的http.response.setHeader方法使用说明
2014/12/14 Javascript
JavaScript中遍历对象的property的3种方法介绍
2014/12/30 Javascript
Bootstrap每天必学之简单入门
2015/11/19 Javascript
全面解析Bootstrap中form、navbar的使用方法
2016/05/30 Javascript
JQ选择器_选择同类元素的第N个子元素的实现方法
2016/09/08 Javascript
使用JavaScript解决网页图片拉伸问题(推荐)
2016/11/25 Javascript
AngularJS 的$timeout服务示例代码
2017/09/21 Javascript
layui之select的option叠加问题的解决方法
2018/03/08 Javascript
关于node-bindings无法在Electron中使用的解决办法
2018/12/18 Javascript
解决vue scoped scss 无效的问题
2020/09/04 Javascript
[02:41]DOTA2亚洲邀请赛小组赛第三日 赛事回顾
2015/02/01 DOTA
Python用GET方法上传文件
2015/03/10 Python
python脚本爬取字体文件的实现方法
2017/04/29 Python
Django 根据数据模型models创建数据表的实例
2018/05/27 Python
深入浅析Python 中 is 语法带来的误解
2019/05/07 Python
用Python实现BP神经网络(附代码)
2019/07/10 Python
python的Jenkins接口调用方式
2020/05/12 Python
Nike挪威官网:Nike.com (NO)
2018/11/26 全球购物
添柏岚英国官方网站:Timberland英国
2019/11/28 全球购物
大学生实习思想汇报
2014/01/12 职场文书
给实习单位的感谢信
2014/02/01 职场文书
物流管理专业毕业生求职信
2014/03/23 职场文书
自我检讨报告
2015/01/28 职场文书
商务邀请函
2015/01/30 职场文书
你会写报告?产品体验报告到底该怎么写?
2019/08/14 职场文书
使用Html+Css实现简易导航栏功能(导航栏遇到鼠标切换背景颜色)
2021/04/07 HTML / CSS
正确的理解和使用Django信号(Signals)
2021/04/14 Python
Python 内置函数速查表一览
2021/06/02 Python
面试分析分布式架构Redis热点key大Value解决方案
2022/03/13 Redis