通常,我们访问实例或类的属性时,将返回所存储的相关值。而特性(property)是一种特殊的属性,访问它时会计算它的值。
请看下面的例子:
import math
class Circle():
def __init__(self, radius):
self.radius = radius # 保存半径
# Circle的一些附加特性
@property
def area(self):
return math.pi * self.radius**2
@property
def perimeter(self):
return 2 * math.pi * self.radius
if __name__ == '__main__':
c = Circle(4.0)
print(f'圆的半径为:{c.radius}')
print(f'圆的面积为:{c.area}')
print(f'圆的周长为:{c.perimeter}')
运行效果如下:
从这个例子中,我们可以看到,Circle实例存储了一个变量c.radius。而c.area和c.perimeter是根据该值计算得来的。@property装饰器支持以简单属性的形式访问后面的方法,无需像平时一样添加额外的()来调用该方法。对象的使用者很难发现正在计算一个属性,除非在试图重新定义该属性时生成了错误消息。
这种特性使用方式遵循所谓的统一访问原则。实际上,如果定义一个类,尽可能保持编程接口的统一总是不错的。如果没有特性(property),将会以简单属性(如c.radius)的形式访问对象的某些属性,而其他属性将以方法(如c.area())的形式进行访问。费力去了解何时添加额外的()会带来不必要的混淆。而特性可以解决此问题。
特性(property)还可以拦截操作,以设置和删除属性。这是通过向特性附加其他的setter和deleter方法来实现的,比如下面的代码:
class Foo():
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('must be a string')
self.__name = value
@name.deleter
def name(self):
raise TypeError('cannot delete name')
f = Foo('lilei')
n = f.name # 调用f.name() get函数
f.name = 'hanmeimei' # 调用setter name(f, 'hanmeimei')
f.name = 14 # 调用setter name(f, 45) -> TypeError
del f.name # 调用deleter name(f) -> TypeError
在这个例子中,实现使用@property装饰器和相关方法将属性name定义为只读特性。后面的@name.setter和@name.deleter装饰器将其他方法与name属性上的设置和删除操作相关联。这些方法的名称必须与原始特性的名称完全匹配。在这些方法中,请注意实际的名称值是存储在属性self.__name中。所存储属性的名称无需遵循任何约定,但它必须与特性名称不同,以便将它与特性的名称区分开来。
以前的老代码中可能还会看到以下这种写法:
class Foo():
def getname(self):
return self.__name
def setname(self, value):
self.__name = value
def delname(self):
raise TypeError('cannot delete name')
name = property(getname, setname, delname)
这种老的写法仍然可用,但新的装饰器版本会让类看起来更完美一些。例如,如果使用装饰器,get、set、delete这些函数将不会显示为方法。