Python @property 装饰器

Python 编程提供了内置的 @property 装饰器,它使得在面向对象编程中,getter 和 setter 的使用变得更加容易。

在深入了解 @property 装饰器的细节之前,我们首先要理解为什么最初需要它。


没有 Getter 和 Setter 的类

假设我们决定创建一个来存储摄氏温度。并且,它还会实现一个方法来将温度转换为华氏温度。

一种实现方式如下:

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

我们可以从这个类创建对象,并随意操作 temperature 属性:

# Basic method of setting and getting attributes in Python
class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32


# Create a new object
human = Celsius()

# Set the temperature
human.temperature = 37

# Get the temperature attribute
print(human.temperature)

# Get the to_fahrenheit method
print(human.to_fahrenheit())

输出

37
98.60000000000001

这里,转换为华氏温度时出现额外的十进制数字是由于浮点运算误差

因此,每当我们像上面那样赋值或检索任何对象属性(如 temperature)时,Python 会在对象的内置 __dict__ 字典属性中搜索它,如下所示:

print(human.__dict__) 
# Output: {'temperature': 37}

因此,human.temperature 在内部变成了 human.__dict__['temperature']


使用 Getter 和 Setter

假设我们想扩展上面定义的 Celsius 类的可用性。我们知道任何物体的温度都不能低于 -273.15 摄氏度。

让我们更新代码以实现此值约束。

上述限制的一个明显解决方案是隐藏属性 temperature(使其成为私有),并定义新的 getter 和 setter 方法来操作它。

可以这样实现:

# Making Getters and Setter methods
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。开头的下划线 _ 用于表示 Python 中的私有变量

现在,让我们使用这个实现:

# Making Getters and Setter methods
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


# Create a new object, set_temperature() internally called by __init__
human = Celsius(37)

# Get the temperature attribute via a getter
print(human.get_temperature())

# Get the to_fahrenheit method, get_temperature() called by the method itself
print(human.to_fahrenheit())

# new constraint implementation
human.set_temperature(-300)

# Get the to_fahreheit method
print(human.to_fahrenheit())

输出

37
98.60000000000001
Traceback (most recent call last):
  File "<string>", line 30, in <module>
  File "<string>", line 16, in set_temperature
ValueError: Temperature below -273.15 is not possible.

此更新成功实现了新的限制。我们不再允许将温度设置为低于 -273.15 摄氏度。

注意:私有变量在 Python 中实际上并不存在。这只是一些需要遵循的规范。语言本身不施加任何限制。

然而,上述更新的更大问题是,所有实现我们以前类的程序都必须将其代码从 obj.temperature 修改为 obj.get_temperature(),并将所有类似 obj.temperature = val 的表达式修改为 obj.set_temperature(val)

这种重构在处理成千上万行代码时可能会导致问题。

总而言之,我们的新更新不向后兼容。这就是 @property 派上用场的地方。


Property 类

解决上述问题的一种 Pythonic 方法是使用 property 类。下面是我们如何更新代码:

# using property class
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

    # creating a property object
    temperature = property(get_temperature, set_temperature)

我们在 get_temperature()set_temperature() 中添加了 print() 函数,以便清楚地观察它们正在执行。

代码的最后一行创建了一个属性对象 temperature。简单来说,property 将一些代码(get_temperatureset_temperature)附加到成员属性访问(temperature)。

让我们使用这个更新后的代码:

# using property class
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

    # creating a property object
    temperature = property(get_temperature, set_temperature)


human = Celsius(37)

print(human.temperature)

print(human.to_fahrenheit())

human.temperature = -300

输出

Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
  File "<string>", line 31, in <module>
  File "<string>", line 18, in set_temperature
ValueError: Temperature below -273 is not possible

如我们所见,任何检索 temperature 值的代码都将自动调用 get_temperature(),而不是进行字典(__dict__)查找。

类似地,任何为 temperature 赋值的代码都将自动调用 set_temperature()

我们甚至可以在上面看到,即使我们创建了一个对象,set_temperature() 也被调用了。

human = Celsius(37) # prints Setting value...

你能猜到为什么吗?

原因在于当一个对象被创建时,__init__() 方法会被调用。这个方法包含行 self.temperature = temperature。这个表达式会自动调用 set_temperature()

同样,任何类似 c.temperature 的访问都会自动调用 get_temperature()。这就是 property 所做的。

通过使用 property,我们可以看到在值约束的实现中不需要任何修改。因此,我们的实现是向后兼容的。

注意:实际的温度值存储在私有变量 _temperature 中。temperature 属性是一个 property 对象,它提供了访问此私有变量的接口。


@property 装饰器

在 Python 中,property() 是一个内置函数,用于创建并返回一个 property 对象。此函数的语法是:

property(fget=None, fset=None, fdel=None, doc=None)

这里,

  • fget 是获取属性值的函数
  • fset 是设置属性值的函数
  • fdel 是删除属性的函数
  • doc 是一个字符串(类似于注释)

从实现中可以看出,这些函数参数是可选的。

一个 property 对象有三个方法:getter()setter()deleter(),用于在稍后指定 fgetfsetfdel。这意味着,这行代码:

temperature = property(get_temperature,set_temperature)

可以分解为:

# make empty property
temperature = property()

# assign fget
temperature = temperature.getter(get_temperature)

# assign fset
temperature = temperature.setter(set_temperature)

这两段代码是等价的。

熟悉Python 装饰器的程序员会发现,上述构造可以作为装饰器来实现。

我们甚至可以不定义 get_temperatureset_temperature 这两个名称,因为它们是不必要的,并且会污染类的命名空间

为此,我们在定义 getter 和 setter 函数时重用 temperature 名称。让我们看看如何将其实现为装饰器:

class Celsius:
    def __init__(self, temperature=0):
        # when creating the object, the setter method is called automatically
        self.temperature = temperature

    def to_fahrenheit(self):
        # convert the temperature to Fahrenheit
        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...")
        # ensure the temperature does not go below absolute zero
        if value < -273.15:
            raise ValueError("Temperature below -273.15°C is not possible")
        self._temperature = value


# create an object with a valid temperature
human = Celsius(37)

# print the temperature in Celsius
print(human.temperature)

# print the temperature in Fahrenheit
print(human.to_fahrenheit())

# attempting to create an object with a temperature below -273.15°C will raise an exception
try:
    coldest_thing = Celsius(-300)
except ValueError as e:
    print(e)

输出

Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
ValueError: Temperature below -273 is not possible

上述实现简单高效。它是使用 property 的推荐方式。

你觉得这篇文章有帮助吗?

我们的高级学习平台,凭借十多年的经验和数千条反馈创建。

以前所未有的方式学习和提高您的编程技能。

试用 Programiz PRO
  • 交互式课程
  • 证书
  • AI 帮助
  • 2000+ 挑战