Blog

Python 用 @property 代替 getter 和 setter

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 习惯,同样牺牲清晰度,同样换不来任何东西。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。