Python 的描述符 descriptor详解
Python在2.2版本中引入了descriptor(描述符)功能,也正是基于这个功能实现了新式类(new-styelclass)的对象模型,同时解决了之前版本中经典类(classicclass)系统中出现的多重继承中的MRO(MethodResolutionOrder)问题,另外还引入了一些新的概念,比如classmethod,staticmethod,super,Property等。因此理解descriptor有助于更好地了解Python的运行机制。
那么什么是descriptor呢?
简而言之:descriptor就是一类实现了__get__(),__set__(),__delete__()方法的对象。
Orz...如果你瞬间顿悟了,那么请收下我的膝盖;
O_o!...如果似懂非懂,那么恭喜你!说明你潜力很大,咱们可以继续挖掘:
引言
对于陌生的事物,一个具体的栗子是最好的学习方式,首先来看这样一个问题:假设我们给一次数学考试创建一个类,用于记录每个学生的学号、数学成绩、以及提供一个用于判断是否通过考试的check函数:
classMathScore(): def__init__(self,std_id,score): self.std_id=std_id self.score=score defcheck(self): ifself.score>=60: return'pass' else: return'failed'
很简单一个示例,看起来运行的不错:
xiaoming=MathScore(10,90) xiaoming.score Out[3]:90 xiaoming.std_id Out[4]:10 xiaoming.check() Out[5]:'pass'
但是会有一个问题,比如手一抖录入了一个负分数,那么他就得悲剧的挂了:
xiaoming=MathScore(10,-90) xiaoming.score Out[8]:-90 xiaoming.check() Out[9]:'failed'
这显然是一个严重的问题,怎么能让一个数学90+的孩子挂科呢,于是乎一个简单粗暴的方法就诞生了:
classMathScore(): def__init__(self,std_id,score): self.std_id=std_id ifscore<0: raiseValueError("Scorecan'tbenegativenumber!") self.score=score defcheck(self): ifself.score>=60: return'pass' else: return'failed'
上面再类的初始化函数中增加了负数判断,虽然不够优雅,甚至有点拙劣,但这在实例初始化时确实工作的不错:
xiaoming=MathScore(10,-90) Traceback(mostrecentcalllast): File"<ipython-input-12-6faad631790d>",line1,in<module> xiaoming=MathScore(10,-90) File"C:/Users/xu_zh/.spyder2-py3/temp.py",line14,in__init__ raiseValueError("Scorecan'tbenegativenumber!") ValueError:Scorecan'tbenegativenumber!
OK,但我们还无法阻止实例对score的赋值操作,毕竟修改成绩也是常有的事:
xiaoming=MathScore(10,90) xiaoming=-10#无法判断出错误
对于大多数童鞋,这个问题soeasy的啦:将score变为私有,从而禁止xiaoming.score这样的直接调用,增加一个get_score和set_score用于读写:
classMathScore(): def__init__(self,std_id,score): self.std_id=std_id ifscore<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=score defcheck(self): ifself.__score>=60: return'pass' else: return'failed' defget_score(self): returnself.__score defset_score(self,value): ifvalue<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=value
这确实是种常见的解决方法,但是不得不说这简直丑爆了:
调用成绩再也不能使用xiaoming.score这样自然的方式,需要使用xiaoming.get_score(),这看起来像口吃在说话!
还有那反人类的下划线和括号...那应该只出现在计算机之间窃窃私语之中...
赋值也无法使用xiaoming.score=80,而需使用xiaoming.set_score(80),这对数学老师来说,太TM不自然了!!!
作为一门简洁优雅的编程语言,Python是不会坐视不管的,于是其给出了Property类:
Property类
先不管Property是啥,咱先看看它是如何简洁优雅的解决上面这个问题的:
classMathScore(): def__init__(self,std_id,score): self.std_id=std_id ifscore<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=score defcheck(self): ifself.__score>=60: return'pass' else: return'failed' def__get_score__(self): returnself.__score def__set_score__(self,value): ifvalue<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=value score=property(__get_score__,__set_score__)
与上段代码相比,主要是在最后一句实例化了一个property实例,并取名为score,这个时候,我们就能如此自然的对instance.__score进行读写了:
xiaoming=MathScore(10,90) xiaoming.score Out[30]:90 xiaoming.score=80 xiaoming.score Out[32]:80 xiaoming.score=-90 Traceback(mostrecentcalllast): File"<ipython-input-33-aed7397ed552>",line1,in<module> xiaoming.score=-90 File"C:/Users/xu_zh/.spyder2-py3/temp.py",line28,in__set_score__ raiseValueError("Scorecan'tbenegativenumber!") ValueError:Scorecan'tbenegativenumber!
WOW~~一切工作正常!
嗯,那么问题来了:它是怎么工作的呢?
先看下property的参数:
classproperty(fget=None,fset=None,fdel=None,doc=None) #拷贝自Python官方文档
它的工作方式:
实例化property实例(我知道这是句废话);
调用property实例(比如xiaoming.score)会直接调用fget,并由fget返回相应值;
对property实例进行赋值操作(xiaoming.score=80)则会调用fset,并由fset定义完成相应操作;
删除property实例(delxiaoming),则会调用fdel实现该实例的删除;
doc则是该property实例的字符说明;
fget/fset/fdel/doc需自定义,如果只设置了fget,则该实例为只读对象;
这看起来和本篇开头所说的descriptor的功能非常相似,让我们回顾一下descriptor:
“descriptor就是一类实现了__get__(),__set__(),__delete__()方法的对象。”
@~@如果你这次又秒懂了,那么请再次收下我的膝盖Orz...
另外,Property还有个装饰器语法糖@property,其所实现的功能与property()完全一样:
classMathScore(): def__init__(self,std_id,score): self.std_id=std_id ifscore<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=score defcheck(self): ifself.__score>=60: return'pass' else: return'failed' @property defscore(self): returnself.__score @score.setter defscore(self,value):#注意方法名称要与上面一致,否则会失效 ifvalue<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=value
我们知道了property实例的工作方式了,那么问题又来了:它是怎么实现的?
事实上Property确实是基于descriptor而实现的,下面进入我们的正题descriptor吧!
descriptor描述符
照样先不管descriptor是啥,咱们还是先看栗子,对于上面Property实现的功能,我们可以通过自定义的descriptor来实现:
classNonNegative(): def__init__(self): pass def__get__(self,ist,cls): return'descriptorget:'+str(ist.__score)#这里加上字符描述便于看清调用 def__set__(self,ist,value): ifvalue<0: raiseValueError("Scorecan'tbenegativenumber!") print('descriptorset:',value) ist.__score=value classMathScore(): score=NonNegative() def__init__(self,std_id,score): self.std_id=std_id ifscore<0: raiseValueError("Scorecan'tbenegativenumber!") self.__score=score defcheck(self): ifself.__score>=60: return'pass' else: return'failed'
我们新定义了一个NonNegative类,并在其内实现了__get__、__set__方法,然后在MathScore类中实例化了一个NonNegative的实例score,注意!!!重要的事情说三遍:score实例是MathScore的类属性!!!类属性!!!类属性!!!这个Mathscore.score属性同上面Property的score实例的功能是一样的,只不过Mathscore.score调用的get、set并不定义在Mathscore内,而是定义在NonNegative类中,而NonNegative类就是一个descriptor对象!
纳尼?NonNegative类的定义中可没见到半个“descriptor”的字样,怎么就成了descriptor对象???
淡定!重要的事情这里只说一遍:任何实现__get__,__set__或__delete__方法中一至多个的类,就是descriptor对象。所以NonNegative自然是一个descriptor对象。
那么descriptor对象与普通类比有什么特别之处呢?先不急,来看看上端代码的效果:
xiaoming=MathScore(10,90) xiaoming.score Out[67]:'descriptorget:90' xiaoming.score=80 descriptorset:80 wangerma=MathScore(11,70) wangerma.score Out[70]:'descriptorget:70' wangerma.score=60 Out[70]:descriptorset:60 wangerma.score Out[73]:'descriptorget:60' xiaoming.score Out[74]:'descriptorget:80' xiaoming.score=-90 ValueError:Scorecan'tbenegativenumber!
可以发现,MathScore.score虽然是一个类属性,但它却可以通过实例的进行赋值,且面对不同的MathScore实例xiaoming、wangerma的赋值和调用,并不会产生冲突!因此看起来似乎更类似于MathScore的实例属性,但与实例属性不同的是它并不通过MathScore实例的读写方法操作值,而总是通过NonNegative实例的__get__和__set__对值进行操作,那么它是怎么做到这点的?
注意看__get__、__set__的参数
def__get__(self,ist,cls): #self:descriptor实例本身(如Math.score),ist:调用score的实例(如xiaoming),cls:descriptor实例所在的类(如MathScore)
...
def__set__(self,ist,value): #score就是通过这些传入的ist、cls参数,实现对MathScore及其具体实例属性的调用和改写的
...
OK,现在我们基本搞清了descriptor实例是如何实现对宿主类的实例属性进行模拟的。事实上Property实例的实现方式与上面的NonNegative实例类似。那么我们既然有了Propery,为什么还要去自定义descriptor呢?
答案在于:更加逼真的模拟实例属性(想想MathScore.__init__里面那恶心的判断语句),还有最重要的是:代码重用!!!
简而言之:通过单个descriptor对象,可以更加逼真的模拟实例属性,并且可以实现对宿主类实例的多个实例属性进行操作。
O.O!如果你又秒懂了,那么你可以直接跳到下面写评论了...
看个栗子:假如不仅要判断学生的分数是否为负数,而且还要判学生的学号是否为负值,使用property的实现方式是这样子的:
classMathScore(): def__init__(self,std_id,score): ifstd_id<0: raiseValueError("Can'tbenegativenumber!") self.__std_id=std_id ifscore<0: raiseValueError("Can'tbenegativenumber!") self.__score=score defcheck(self): ifself.__score>=60: return'pass' else: return'failed' @property defscore(self): returnself.__score @score.setter defscore(self,value): ifvalue<0: raiseValueError("Can'tbenegativenumber!") self.__score=value @property defstd_id(self): returnself.__std_id @std_id.setter defstd_id(self,idnum): ifidnum<0: raiseValueError("Can'tbenegativenmuber!") self.__std_id=idnum
Property实例最大的问题是:
无法影响宿主类实例的初始化,所以咱必须在__init__加上那丑恶的if...
单个Property实例仅能针对宿主类实例的单个属性,如果需要对多个属性进行控制,则必须定义多个Property实例,这真是太蛋疼了!
但是自定义descriptor可以很好的解决这个问题,看下实现:
classNonNegative(): def__init__(self): self.dic=dict() def__get__(self,ist,cls): print('Descriptionget',ist) returnself.dic[ist] def__set__(self,ist,value): print('Descriptionset',ist,value) ifvalue<0: raiseValueError("Can'tbenegativenumber!") self.dic[ist]=value classMathScore(): score=NonNegative() std_id=NonNegative() def__init__(self,std_id,score): #这里并未创建实例属性std_id和score,而是调用MathScore.std_id和MathScore.score self.std_id=std_id self.score=score defcheck(self): ifself.score>=60: return'pass' else: return'failed'
哈哈~!MathScore.__init__内终于没了if,代码也比上面的简洁不少,但是功能一个不少,且实例之间不会相互影响:
事实上,MathScore多个实例的同一个属性,都是通过单个MathScore类的相应类属性(也即NonNegative实例)操作的,这同property一致,但它又是怎么克服Property的两个不足的呢?秘诀有三个:
Property实例本质上是借助类属性,变向对实例属性进行操作,而NonNegative实例则是完全通过类属性模拟实例属性,因此实例属性其实根本不存在;
NonNegative实例使用字典记录每个MathScore实例及其对应的属性值,其中key为MathScore实例名:比如score实例就是使用dic={‘Zhangsan':50,‘Lisi':90}记录每个实例对应的score值,从而确保可以实现对MathScore实例属性的模拟;
MathScore通过在__init__内直接调用类属性,从而实现对实例属性初始化赋值的模拟,而Property则不可能,因为Property实例(也即MathScore的类属性)是真实的操作MathScore实例传入的实例属性以达到目的,但如果在初始化程序中传入的不是实例属性,而是类属性(也即Property实例本身),则会陷入无限递归(PS:想一下如果将前一个property实例实现中的self.__score改成这里的self.score会发生什么)。
这三点看的似懂非懂,没关系,来个比喻:
每个descriptor实例(MathScore.score和MathScore.std_id)都是类作用域里的一个篮子,篮子里放着写着每个MathScore实例名字的盒子(‘zhangsan','lisi‘),同一个篮子里的盒子只记录同样属性的值(比如score篮子里的盒子只记录分数值),当MathScore的实例对相应属性进行操作时,则找到对应的篮子,取出标有该实例名字的盒子,并对其进行操作。
因此,实例对应的属性,压根不在实例自己的作用域内,而是在类作用域的篮子里,只不过我们可以通过xiaoming.score这样的方式进行操作而已,所以其实际的调用的逻辑是这样的:下图右侧的实例分别通过红线和黑线对score和std_id进行操作,他们首先通过类调用相应的类属性,然后类属性通过对应的descriptor实例作用域对操作进行处理,并返回给类属性相应结果,最后让实例感知到。
看到这里,很多童鞋可能不淡定了,因为大家都知道在Python中采取xiaoming.score=10这样的赋值方式,如果xiaoming没有score这样的实例属性,必定会自动创建该实例属性,怎么会去调用MathScore的score呢?
首先,要鼓掌!!!给想到这点的童鞋点赞!!!其实上面在说Property的时候这个问题就产生了。
其次,Python为了实现discriptor确实对属性的调用顺序做出了相应的调整,这些将会“Python的descriptor(下)”中介绍。