Python中属性和描述符的正确使用
关于@property装饰器
在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问。
那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口。
举个栗子,假如我们有一个需要表示电影的类:
classMovie(object): def__init__(self,title,description,score,ticket): self.title=title self.description=description self.score=scroe self.ticket=ticket
你开始在项目的其他地方使用这个类,但是之后你意识到:如果不小心给电影打了负分怎么办?你觉得这是错误的行为,希望Movie类可以阻止这个错误。你首先想到的办法是将Movie类修改为这样:
classMovie(object): def__init__(self,title,description,score,ticket): self.title=title self.description=description self.ticket=ticket ifscore<0: raiseValueError("Negativevaluenotallowed:{}".format(score)) self.score=scroe
但这行不通。因为其他部分的代码都是直接通过Movie.score来赋值的。这个新修改的类只会在__init__方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。如果有人试着运行m.scrore=-100,那么谁也没法阻止。那该怎么办?
Python的property解决了这个问题。
我们可以这样做
classMovie(object): def__init__(self,title,description,score): self.title=title self.description=description self.score=score self.ticket=ticket @property defscore(self): returnself.__score @score.setter defscore(self,score): ifscore<0: raiseValueError("Negativevaluenotallowed:{}".format(score)) self.__score=score @score.deleter defscore(self): raiseAttributeError("Cannotdeletescore")
这样在任何地方修改score都会检测它是否小于0。
property的不足
对property来说,最大的缺点就是它们不能重复使用。举个例子,假设你想为ticket字段也添加非负检查。
下面是修改过的新类:
classMovie(object): def__init__(self,title,description,score,ticket): self.title=title self.description=description self.score=score self.ticket=ticket @property defscore(self): returnself.__score @score.setter defscore(self,score): ifscore<0: raiseValueError("Negativevaluenotallowed:{}".format(score)) self.__score=score @score.deleter defscore(self): raiseAttributeError("Cannotdeletescore") @property defticket(self): returnself.__ticket @ticket.setter defticket(self,ticket): ifticket<0: raiseValueError("Negativevaluenotallowed:{}".format(ticket)) self.__ticket=ticket @ticket.deleter defticket(self): raiseAttributeError("Cannotdeleteticket")
可以看到代码增加了不少,但重复的逻辑也出现了不少。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。
描述符登场
什么是描述符?
一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__() 、__set__()和__delete__(),一个对象中只要包含了这三个方法中的至少一个就称它为描述符。
描述符有什么作用?
Thedefaultbehaviorforattributeaccessistoget,set,ordeletetheattributefromanobject'sdictionary.Forinstance,a.xhasalookupchainstartingwitha.__dict__[‘x'],thentype(a).__dict__[‘x'],andcontinuingthroughthebaseclassesoftype(a)excludingmetaclasses.Ifthelooked-upvalueisanobjectdefiningoneofthedescriptormethods,thenPythonmayoverridethedefaultbehaviorandinvokethedescriptormethodinstead.Wherethisoccursintheprecedencechaindependsonwhichdescriptormethodsweredefined.—–摘自官方文档
简单的说描述符会改变一个属性的基本的获取、设置和删除方式。
先看如何用描述符来解决上面property逻辑重复的问题。
classInteger(object): def__init__(self,name): self.name=name def__get__(self,instance,owner): returninstance.__dict__[self.name] def__set__(self,instance,value): ifvalue<0: raiseValueError("Negativevaluenotallowed") instance.__dict__[self.name]=value classMovie(object): score=Integer('score') ticket=Integer('ticket')
因为描述符优先级高并且会改变默认的get、set行为,这样一来,当我们访问或者设置Movie().score的时候都会受到描述符Integer的限制。
不过我们也总不能用下面这样的方式来创建实例。
a=Movie() a.score=1 a.ticket=2 a.title=‘test' a.descript=‘…'
这样太生硬了,所以我们还缺一个构造函数。
classInteger(object): def__init__(self,name): self.name=name def__get__(self,instance,owner): ifinstanceisNone: returnself returninstance.__dict__[self.name] def__set__(self,instance,value): ifvalue<0: raiseValueError('Negativevaluenotallowed') instance.__dict__[self.name]=value classMovie(object): score=Integer('score') ticket=Integer('ticket') def__init__(self,title,description,score,ticket): self.title=title self.description=description self.score=score self.ticket=ticket
这样在获取、设置和删除score和ticket的时候都会进入Integer的__get__、__set__,从而减少了重复的逻辑。
现在虽然问题得到了解决,但是你可能会好奇这个描述符到底是如何工作的。具体来说,在__init__函数里访问的是自己的self.score和self.ticket,怎么和类属性score和ticket关联起来的?
描述符如何工作
看官方的说明
Ifanobjectdefinesboth__get__()and__set__(),itisconsideredadatadescriptor.Descriptorsthatonlydefine__get__()arecallednon-datadescriptors(theyaretypicallyusedformethodsbutotherusesarepossible).
Dataandnon-datadescriptorsdifferinhowoverridesarecalculatedwithrespecttoentriesinaninstance'sdictionary.Ifaninstance'sdictionaryhasanentrywiththesamenameasadatadescriptor,thedatadescriptortakesprecedence.Ifaninstance'sdictionaryhasanentrywiththesamenameasanon-datadescriptor,thedictionaryentrytakesprecedence.
Theimportantpointstorememberare:
descriptorsareinvokedbythe__getattribute__()method
overriding__getattribute__()preventsautomaticdescriptorcalls
object.__getattribute__()andtype.__getattribute__()makedifferentcallsto__get__().
datadescriptorsalwaysoverrideinstancedictionaries.
non-datadescriptorsmaybeoverriddenbyinstancedictionaries.
类调用__getattribute__()的时候大概是下面这样子:
def__getattribute__(self,key): "Emulatetype_getattro()inObjects/typeobject.c" v=object.__getattribute__(self,key) ifhasattr(v,'__get__'): returnv.__get__(None,self) returnv
下面是摘自国外一篇博客上的内容。
GivenaClass“C”andanInstance“c”where“c=C(…)”,calling“c.name”meanslookingupanAttribute“name”ontheInstance“c”likethis:
GettheClassfromInstance
CalltheClass'sspecialmethodgetattribute__.Allobjectshaveadefault__getattribute
Insidegetattribute
GettheClass'smroasClassParents
ForeachClassParentinClassParents
IftheAttributeisintheClassParent'sdict
Ifisadatadescriptor
Returntheresultfromcallingthedatadescriptor'sspecialmethod__get__()
Breaktheforeach(donotcontinuesearchingthesameAttributeanyfurther)
IftheAttributeisinInstance'sdict
Returnthevalueasitis(evenifthevalueisadatadescriptor)
ForeachClassParentinClassParents
IftheAttributeisintheClassParent'sdict
Ifisanon-datadescriptor
Returntheresultfromcallingthenon-datadescriptor'sspecialmethod__get__()
IfitisNOTadescriptor
Returnthevalue
IfClasshasthespecialmethodgetattr
ReturntheresultfromcallingtheClass'sspecialmethod__getattr__.
我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的datadescriptor如果有,就用这个datadescriptor代理该属性,如果没有再寻找该实例自身的__dict__,如果有就返回。任然没有再查找它和它父类里的non-datadescriptor,最后查找是否有__getattr__
描述符的应用场景
python的property、classmethod修饰器本身也是一个描述符,甚至普通的函数也是描述符(non-datadiscriptor)
djangomodel和SQLAlchemy里也有描述符的应用
classUser(db.Model): id=db.Column(db.Integer,primary_key=True) username=db.Column(db.String(80),unique=True) email=db.Column(db.String(120),unique=True) def__init__(self,username,email): self.username=username self.email=email def__repr__(self): return'<User%r>'%self.username
总结
只有当确实需要在访问属性的时候完成一些额外的处理任务时,才应该使用property。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。以上就是本文的全部内容,由于个人能力有限,文中如有笔误、逻辑错误甚至概念性错误,还请提出并指正。