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_temperature
和 set_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()
,用于在稍后指定 fget
、fset
和 fdel
。这意味着,这行代码:
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_temperature
和 set_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
的推荐方式。