Python 用不着 getter 和 setter,因为 @property 让你以后再加校验时,一个调用方都不用改。
凡是有私有字段的语言,都会教你一开始就写好 get_x() 和 set_x(),以防以后要加校验。Python 不需要这样做,而背后的原因值得弄懂,而不是死记。
Python 用不着的习惯
# the Java habit, which Python does not need
class TemperatureJava:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
self._celsius = value
t = TemperatureJava(20)
t.set_celsius(25)
print(t.get_celsius())
输出 25。六行形式主义,除了让调用处更难看,什么也没做。
从普通属性开始
# in Python you start with a plain attribute
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t2 = Temperature(20)
t2.celsius = 25
print(t2.celsius)
输出 25。行为一样,没有形式主义。
反对意见很明显:需要校验的时候怎么办?在没有 property 的语言里,答案是“把所有调用方都改一遍”,所以大家才防御性地写 getter。在 Python 里,答案是不用改。
以后再加 @property,外部什么都不变
# and add @property later, without changing a single caller
class Temperature2:
def __init__(self, celsius):
self.celsius = celsius # goes through the setter below
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"{value} is below absolute zero")
self._celsius = value
t3 = Temperature2(20)
t3.celsius = 25
print(t3.celsius)
try:
t3.celsius = -300
except ValueError as err:
print(type(err).__name__ + ':', err)
输出:
25
ValueError: -300 is below absolute zero
t3.celsius = 25 读起来仍然是属性赋值,因为从语法上看它就是。property 把这次赋值拦了下来。已有的调用方全都照常工作,一行不改。 整个论点就在这里:你可以把决定推迟到真正有理由的时候,因为以后再做也没有任何成本。
注意 __init__ 里赋值的是 self.celsius,不是 self._celsius。这样构造对象时也会经过 setter,非法值在创建时就会被拒绝,而不是只在之后赋值时才拒绝。
计算属性
另一种用法,也是更常见的一种:值由其他值推导出来,所以永远不会过时。
# a computed property: derived, never stored, never stale
class Temperature3:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
t4 = Temperature3(100)
print(t4.fahrenheit)
t4.celsius = 0
print(t4.fahrenheit)
输出:
212.0
32.0
如果把 fahrenheit 存成真正的属性,你就有了两个数据源,只要有人改了其中一个,它们就会对不上。算出来的值不会有这个问题。
顺手得到只读
不写 setter,赋值就会失败:
try:
t4.fahrenheit = 50
except AttributeError as err:
print(type(err).__name__ + ':', err)
输出:
AttributeError: property 'fahrenheit' of 'Temperature3' object has no setter
这条信息来自 Python 3.11 及以后的版本。旧版本显示的是 can't set attribute,意思一样,只是没那么有用。
要点
- 从普通属性开始。永远如此。
- 真正需要校验或计算时再加
@property——调用方完全察觉不到。 - 在
__init__里通过 property 赋值,这样构造时也会校验。 - 没有 setter 的 property 是只读的,这是保护推导值成本最低的办法。
唯一不该做的,是“以防万一”把每个属性都包成 property。那只是换了个好看语法的 getter 习惯,同样牺牲清晰度,同样换不来任何东西。