翻译自:https://www.programiz.com/python-programming/property。
1、没有getters和setters的类
假设我们想要实现一个类来存储摄氏温度,并且这个类提供一个方法能够将摄氏温度转化为华氏温度。一种直观且快速的想法是:
class Celsius:
def __init__(self, temperature = 0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
那么,我们可以按照如下的方式使用这个类:
# 创建一个对象
human = Celsius()
# 设置温度
human.temperature = 37
# 获取温度的值
print(human.temperature)
# 转化为华氏度
print(human.to_fahrenheit())
输出:
37
98.60000000000001
实事上,当我们对实例的属性(如上述temperature
)进行赋值或者取值操作时,Python会在该对象内置的字典对象__dict__
查找对应的属性。
>>> human.__dict__
{'temperature': 37}
所以,human.temperature
实际上与human.__dict__['temperature']
是等价的。
2、使用getters和setters
现在,我们要扩展一下上述Celsius
的功能。我们知道,任何物体都无法达到绝对零度(-273.15℃),因此,在给temperature
属性赋值时,需要加上这个限制。
一种显而易见的优化方式是,将temperature
属性隐藏(设置为受保护的属性),然后定义新的getter和setter方法来操作temperature
属性。代码如下:
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter method
def get_temperature(self):
return self._temperature
# setter method
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
引入的get_temperature()
和set_temperature()
属性,分别用于获取_temperature
的值和向_temperature
赋值。
另外,temperature
被替换成了_temperature
,以_
开头的变量,属于类的私有变量。
我们可以按如下方式来使用这个类:
human = Celsius(37)
# 获取 _temperature
print(human.get_temperature())
# 转化为华氏度
print(human.to_fahrenheit())
# 设置 _temperature
human.set_temperature(-300)
输出如下:
37
98.60000000000001
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 15, in set_temperature
ValueError: Temperature below -273.15 is not possible.
可以看出,当尝试将温度设置为-300时,会触发程序里面设置的限制规则。
注意:所谓的私有变量,更多的是作为Python编程中的一种约定,而没有限制用户对它的操作,因为用户仍然可以去修改变量的值:
>>>human._temperature = -300 >>>human.get_temperature() -300
上述方法确实是有效的,但需要花费的代价有点大:我们需要将之前程序中所有的obj.temperature
改为obj.get_temperature()
,所有的obj.temperature = val
改为obj.set_temperature(val)
。当你需要更该成百上千行这样的代码时,你肯定会感觉烦躁。
也就是说,我们的更改不是向后兼容的。这时,就到了@property
大显身手的时候了。
3、property
类
使用property
类,可以让我们的代码更加的Pythonic:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
print("Setting value success")
# creating a property object
temperature = property(get_temperature, set_temperature)
通过在get_temperature()
和set_temperature()
加入打印信息,可以更加清楚地看到代码的调用顺序。最后一行,实际上是通过property
类实例化了一个对象temperature
,简单来说,property
将一些方法附加到成员属性上进行访问。
按如下形式调用上述代码:
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
human.temperature = -300
输出如下:
Setting value...
Setting value success
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 18, in set_temperature
ValueError: Temperature below -273.15 is not possible
可以看到,任何尝试获取temperature
值的代码都会调用get_temperature()
方法,而不是从__dict__
中取值;类似的,任何尝试向temperature
赋值的代码都会调用set_temperature()
方法(可以看到,在用Celsius
实例化一个对象时,也是如此)。
通过使用property
,我们无需更改原先的代码就实现了温度值的验证,也就是说,我们实现了向后兼容。
注意:温度的值实际上存储在私有变量
_temperature
所指向的内存空间中,而temperature
只是一个为该私有变量提供了一个调用接口而已。
4、@property
装饰器
在Python中,property()
是一个用于创建property
对象的内建类,其调用格式为:
property(fget=None, fset=None, fdel=None, doc=None)
其中,
fget
:指代用于获取属性值的函数fset
:指代用于设置属性值的函数fdel
:指代用户删除属性的函数doc
:表示注释的字符串
在使用时,可以先实例化一个空的property
对象,然后再调用property
对象的getter()
、setter()
和delter()
方法来指代fget
、fset
和fdel
。也就是说,下面的写法:
temperature = property()
temperature = temperature.getter(get_temperature)
temperature = temperature.setter(set_temperature)
和
temperature = property(get_temperature,set_temperature)
是等价的。
对于熟悉Python装饰器的程序员来说,上述功能完全可以用装饰器来实现。简单来说,装饰器的作用就是对其作用域内的函数进行「装饰」,例如,可以用装饰器来将上述代码改写成下面的样子:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273 is not possible")
self._temperature = value
print("Setting value success")
在上面的代码里,在定义取值和赋值操作时,我们重新使用了变量名temperature
。
同样,使用方式也是向后兼容的:
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
coldest_thing = Celsius(-300)
输出:
Setting value...
Setting value success
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "<input>", line 7, in <module>
File "<input>", line 3, in __init__
File "<input>", line 17, in temperature
ValueError: Temperature below -273 is not possible