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。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。以上就是本文的全部内容,由于个人能力有限,文中如有笔误、逻辑错误甚至概念性错误,还请提出并指正。