跳到主要内容

Python @property 装饰器

提示
  1. @property简化属性访问:Python的@property装饰器使得在面向对象编程中使用getter和setter方法更简单,提高了代码的可读性和易用性。
  2. 封装和验证:通过getter和setter方法,@property装饰器允许封装私有变量(如_temperature),并在设置属性时实现验证(如温度不能低于-273.15摄氏度)。
  3. 向后兼容性:使用@property装饰器可以保持代码的向后兼容性,即使在类中添加了新的约束或修改,也不需要改变现有代码中对属性的访问方式。

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

在详细介绍@property装饰器是什么之前,让我们先了解为什么首先需要它。

没有Getters和Setters的类

假设我们决定创建一个,用于存储摄氏度温度,并实现一个将温度转换为华氏度的方法。

实现这一功能的一种方式如下:

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

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

我们可以创建这个类的对象,并按我们希望的方式操纵temperature属性:

# Python中设置和获取属性的基本方法
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)

# 获取to_fahrenheit方法
print(human.to_fahrenheit())

输出

37
98.60000000000001

这里,华氏度转换中的额外小数位是由于Floating Point Arithmetic Error

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

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

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

使用Getters和Setters

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

让我们更新我们的代码来实现这个值约束。

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

这可以如下实现:

# 创建Getters和Setter方法
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)

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

# getter方法
def get_temperature(self):
return self._temperature

# setter方法
def set_temperature(self, value):
if value < -273.15:
raise ValueError("温度低于-273.15是不可能的。")
self._temperature = value

正如我们所见,上述方法引入了两个新的get_temperature()set_temperature()方法。

此外,temperature被替换为_temperature。在Python中,开头的下划线_用于表示私有变量。

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

# 创建Getters和Setter方法
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)

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

# getter方法
def get_temperature(self):
return self._temperature

# setter方法
def set_temperature(self, value):
if value < -273.15:
raise ValueError("温度低于-273.15是不可能的。")
self._temperature = value

# 创建一个新对象,__init__内部调用set_temperature()
human = Celsius(37)

# 通过getter获取温度属性
print(human.get_temperature())

# 获取to_fahrenheit方法,方法本身调用get_temperature()
print(human.to_fahrenheit())

# 实现新的约束
human.set_temperature(-300)

# 获取to_fahreheit方法
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: 温度低于 -273.15 是不可能的。

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

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

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

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

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

property类

处理上述问题的一种Python风格的方法是使用property类。以下是我们如何更新代码:

# 使用property类
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("获取值...")
return self._temperature

# setter
def set_temperature(self, value):
print("设置值...")
if value < -273.15:
raise ValueError("温度低于-273.15是不可能的")
self._temperature = value

# 创建一个property对象
temperature = property(get_temperature, set_temperature)

我们在get_temperature()set_temperature()内部添加了print()函数,以清楚地观察到它们被执行。

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

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

# 使用property类
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("获取值...")
return self._temperature

# setter
def set_temperature(self, value):
print("设置值...")
if value < -273.15:
raise ValueError("温度低于-273.15是不可能的")
self._temperature = value

# 创建一个property对象
temperature = property(get_temperature, set_temperature)

human = Celsius(37)

print(human.temperature)

print(human.to_fahrenheit())

human.temperature = -300

输出

设置值...
获取值...
37
获取值...
98.60000000000001
设置值...
Traceback (most recent call last):
File "<string>", line 31, in <module>
File "<string>", line 18, in set_temperature
ValueError: 温度低于 -273 是不可能的

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

类似地,任何给temperature分配值的代码将自动调用set_temperature()

我们甚至可以看到,在我们创建对象时set_temperature()就被调用了。

human = Celsius(37) # 打印设置值...

你能猜出为什么吗?

原因是当一个对象被创建时,__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)

可以被拆分为:

# 创建空的property
temperature = property()

# 分配fget
temperature = temperature.getter(get_temperature)

# 分配fset
temperature = temperature.setter(set_temperature)

这两段代码是等效的。

熟悉Python装饰器的程序员可以认出,上述结构可以作为装饰器实现。

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

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

# 使用@property装饰器
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("获取值...")
return self._temperature

@temperature.setter
def temperature(self, value):
print("设置值...")
if value < -273.15:
raise ValueError("温度低于-273是不可能的")
self._temperature = value

# 创建一个对象
human = Celsius(37)

print(human.temperature)

print(human.to_fahrenheit())

coldest_thing = Celsius(-300)

输出

设置值...
获取值...
37
获取值...
98.60000000000001
设置值...
Traceback (most recent call last):
File "", line 29, in
File "", line 4, in __init__
File "", line 18, in temperature
ValueError: 温度低于 -273 是不可能的

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