Python中怎么给属性增加类型检查或合法性验证?
问题
你想给某个实例attribute增加除访问与修改之外的其他处理逻辑,比如类型检查或合法性验证。解决方案
自定义某个属性的一种简单方法是将它定义为一个property。例如,下面的代码定义了一个property,增加对一个属性简单的类型检查:classPerson:definit(self,firstname):self。firstnamefirstnameGetterfunctionpropertydeffirstname(self):returnself。firstnameSetterfunctionfirstname。setterdeffirstname(self,value):ifnotisinstance(value,str):raiseTypeError(Expectedastring)self。firstnamevalueDeleterfunction(optional)firstname。deleterdeffirstname(self):raiseAttributeError(Cantdeleteattribute)
上述代码中有三个相关联的方法,这三个方法的名字都必须一样。第一个方法是一个getter函数,它使得firstname成为一个属性。其他两个方法给firstname属性添加了setter和deleter函数。需要强调的是只有在firstname属性被创建后,后面的两个装饰器firstname。setter和firstname。deleter才能被定义。property的一个关键特征是它看上去跟普通的attribute没什么两样,但是访问它的时候会自动触发getter、setter和deleter方法。例如:aPerson(Guido)a。firstnameCallsthegetterGuidoa。firstname42CallsthesetterTraceback(mostrecentcalllast):Filestdin,line1,inmoduleFileprop。py,line14,infirstnameraiseTypeError(Expectedastring)TypeError:Expectedastringdela。firstnameTraceback(mostrecentcalllast):Filestdin,line1,inmoduleAttributeError:cantdeleteattribute
在实现一个property的时候,底层数据(如果有的话)仍然需要存储在某个地方。因此,在get和set方法中,你会看到对firstname属性的操作,这也是实际数据保存的地方。另外,你可能还会问为什么init()方法中设置了self。firstname而不是self。firstname。在这个例子中,我们创建一个property的目的就是在设置attribute的时候进行检查。因此,你可能想在初始化的时候也进行这种类型检查。通过设置self。firstname,自动调用setter方法,这个方法里面会进行参数的检查,否则就是直接访问self。firstname了。还能在已存在的get和set方法基础上定义property。例如:classPerson:definit(self,firstname):self。setfirstname(firstname)Getterfunctiondefgetfirstname(self):returnself。firstnameSetterfunctiondefsetfirstname(self,value):ifnotisinstance(value,str):raiseTypeError(Expectedastring)self。firstnamevalueDeleterfunction(optional)defdelfirstname(self):raiseAttributeError(Cantdeleteattribute)Makeapropertyfromexistinggetsetmethodsnameproperty(getfirstname,setfirstname,delfirstname)讨论
一个property属性其实就是一系列相关绑定方法的集合。如果你去查看拥有property的类,就会发现property本身的fget、fset和fdel属性就是类里面的普通方法。比如:Person。firstname。fgetfunctionPerson。firstnameat0x1006a60e0Person。firstname。fsetfunctionPerson。firstnameat0x1006a6170Person。firstname。fdelfunctionPerson。firstnameat0x1006a62e0
通常来讲,你不会直接去调用fget或者fset,它们会在访问property的时候自动被触发。只有当你确实需要对attribute执行其他额外的操作的时候才应该使用到property。有时候一些从其他编程语言(比如Java)过来的程序员总认为所有访问都应该通过getter和setter,所以他们认为代码应该像下面这样写:classPerson:definit(self,firstname):self。firstnamefirstnamepropertydeffirstname(self):returnself。firstnamefirstname。setterdeffirstname(self,value):self。firstnamevalue
不要写这种没有做任何其他额外操作的property。首先,它会让你的代码变得很臃肿,并且还会迷惑阅读者。其次,它还会让你的程序运行起来变慢很多。最后,这样的设计并没有带来任何的好处。特别是当你以后想给普通attribute访问添加额外的处理逻辑的时候,你可以将它变成一个property而无需改变原来的代码。因为访问attribute的代码还是保持原样。Properties还是一种定义动态计算attribute的方法。这种类型的attributes并不会被实际的存储,而是在需要的时候计算出来。比如:importmathclassCircle:definit(self,radius):self。radiusradiuspropertydefarea(self):returnmath。piself。radius2propertydefdiameter(self):returnself。radius2propertydefperimeter(self):return2math。piself。radius
在这里,我们通过使用properties,将所有的访问接口形式统一起来,对半径、直径、周长和面积的访问都是通过属性访问,就跟访问简单的attribute是一样的。如果不这样做的话,那么就要在代码中混合使用简单属性访问和方法调用。下面是使用的实例:cCircle(4。0)c。radius4。0c。areaNoticelackof()50。26548245743669c。perimeterNoticelackof()25。132741228718345
尽管properties可以实现优雅的编程接口,但有些时候你还是会想直接使用getter和setter函数。例如:pPerson(Guido)p。getfirstname()Guidop。setfirstname(Larry)
这种情况的出现通常是因为Python代码被集成到一个大型基础平台架构或程序中。例如,有可能是一个Python类准备加入到一个基于远程过程调用的大型分布式系统中。这种情况下,直接使用getset方法(普通方法调用)而不是property或许会更容易兼容。最后一点,不要像下面这样写有大量重复代码的property定义:classPerson:definit(self,firstname,lastname):self。firstnamefirstnameself。lastnamelastnamepropertydeffirstname(self):returnself。firstnamefirstname。setterdeffirstname(self,value):ifnotisinstance(value,str):raiseTypeError(Expectedastring)self。firstnamevalueRepeatedpropertycode,butforadifferentname(bad!)propertydeflastname(self):returnself。lastnamelastname。setterdeflastname(self,value):ifnotisinstance(value,str):raiseTypeError(Expectedastring)self。lastnamevalue
重复代码会导致臃肿、易出错和丑陋的程序。好消息是,通过使用装饰器或闭包,有很多种更好的方法来完成同样的事情。