一、什么是 @property?
@property 是 Python 内置的装饰器,它可以将类的方法变成属性来访问,从而在不改变调用方式的情况下,实现属性的访问控制、验证和计算。简单来说,它让你可以像使用普通属性一样调用方法,而在背后可以添加额外的逻辑。
classPerson:
def__init__(self, name, age):
self.name = name
self._age = age # 受保护的属性
@property
defage(self): # 变成属性
returnself._age
p = Person("张三", 25)
print(p.age) # 像属性一样访问,不需要加括号
# p.age = 30 # 会报错:没有 setter
二、为什么需要 @property?
2.1 封装和访问控制
通过 @property,你可以将属性隐藏为“私有”,然后通过公开的 getter/setter 方法控制访问和修改,从而避免直接暴露内部属性。
2.2 数据验证
在设置属性时,可以添加验证逻辑,防止无效数据被赋值。
2.3 计算属性
可以根据其他属性动态计算出属性值,而不需要存储额外数据。
2.4 保持接口一致性
最初可能只是普通属性,后来需要添加逻辑,使用 @property 可以保持调用方式不变,不需要修改调用代码。
# 最初版本
classCircle:
def__init__(self, radius):
self.radius = radius
# 后来需要添加验证,但仍然希望使用 circle.radius = 5 的方式
classCircle:
def__init__(self, radius):
self._radius = radius
@property
defradius(self):
returnself._radius
@radius.setter
defradius(self, value):
if value <= 0:
raise ValueError("半径必须为正数")
self._radius = value
三、@property 的基本用法
3.1 getter(获取属性)
使用 @property 装饰方法,可以让方法像属性一样被访问。
classRectangle:
def__init__(self, width, height):
self.width = width
self.height = height
@property
defarea(self):
returnself.width * self.height
rect = Rectangle(4, 5)
print(rect.area) # 20,不需要括号
3.2 setter(设置属性)
使用 @属性名.setter 装饰器定义 setter 方法,可以在赋值时进行验证或处理。
classStudent:
def__init__(self, name, score):
self.name = name
self._score = score
@property
defscore(self):
returnself._score
@score.setter
defscore(self, value):
ifnotisinstance(value, (int, float)):
raise TypeError("成绩必须是数字")
if value < 0or value > 100:
raise ValueError("成绩必须在0-100之间")
self._score = value
s = Student("小明", 85)
print(s.score) # 85
s.score = 92# 使用 setter
print(s.score) # 92
# s.score = 200 # ValueError
3.3 deleter(删除属性)
使用 @属性名.deleter 装饰器定义 deleter 方法,可以控制属性的删除行为。
classTempFile:
def__init__(self, filename):
self.filename = filename
self._content = "some data"
@property
defcontent(self):
returnself._content
@content.deleter
defcontent(self):
print(f"删除 {self.filename} 的内容")
self._content = None
tf = TempFile("data.txt")
print(tf.content) # some data
del tf.content # 删除 data.txt 的内容
print(tf.content) # None
四、计算属性
@property 非常适合定义派生属性,即根据其他属性计算出来的值。
classOrder:
def__init__(self, price, quantity):
self.price = price
self.quantity = quantity
@property
deftotal(self):
returnself.price * self.quantity
@property
defformatted_total(self):
returnf"¥{self.total:.2f}"
order = Order(29.9, 3)
print(order.total) # 89.7
print(order.formatted_total) # ¥89.70
当 price 或 quantity 改变时,total 会自动重新计算。
五、实战案例
5.1 温度转换器
classTemperature:
def__init__(self, celsius):
self._celsius = celsius
@property
defcelsius(self):
returnself._celsius
@celsius.setter
defcelsius(self, value):
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
@property
deffahrenheit(self):
returnself._celsius * 9/5 + 32
@fahrenheit.setter
deffahrenheit(self, value):
self.celsius = (value - 32) * 5/9
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 100
print(temp.celsius) # 37.777...
5.2 用户年龄验证
classUser:
def__init__(self, name, age):
self.name = name
self._age = age
@property
defage(self):
returnself._age
@age.setter
defage(self, value):
ifnotisinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0:
raise ValueError("年龄不能为负数")
if value > 150:
raise ValueError("年龄不能超过150")
self._age = value
@property
defcan_vote(self):
returnself.age >= 18
user = User("张三", 20)
print(user.can_vote) # True
user.age = 16
print(user.can_vote) # False
5.3 懒加载属性
import time
classDataProcessor:
def__init__(self, data):
self.data = data
self._result = None
@property
defprocessed_result(self):
ifself._result isNone:
print("正在处理数据...")
time.sleep(2) # 模拟耗时计算
self._result = sum(self.data) / len(self.data)
returnself._result
dp = DataProcessor([1, 2, 3, 4, 5])
print(dp.processed_result) # 第一次计算,输出 "正在处理数据..."
print(dp.processed_result) # 第二次直接返回缓存值
5.4 只读属性
如果只定义 @property 而不定义 setter,属性就是只读的。
classCircle:
def__init__(self, radius):
self._radius = radius
@property
defradius(self):
returnself._radius
@property
defarea(self):
return3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53975
# c.radius = 10 # AttributeError: can't set attribute
六、注意事项
6.1 私有属性命名惯例
通常使用单下划线 _ 开头的属性名(如 _age)来表示“受保护”的属性,不建议直接访问。
6.2 避免无限递归
在 getter 或 setter 内部,不要直接访问同名的属性,否则会再次触发 getter/setter,导致无限递归。
# 错误示例
classBad:
def__init__(self, value):
self.value = value # 这会调用 setter,而 setter 又调用 self.value,无限递归
@property
defvalue(self):
returnself._value
@value.setter
defvalue(self, val):
self.value = val # 错误!应该用 self._value = val
6.3 性能考虑
计算属性每次访问都会执行计算,如果计算开销大,可以考虑缓存结果(如懒加载模式)。
6.4 不要过度使用
对于简单的属性(没有验证或计算需求),直接使用普通属性即可,不需要 @property。
七、总结
- •
@property 装饰器将方法变成属性,提供 getter、setter、deleter 的功能。 - • 优点:实现封装、数据验证、计算属性,同时保持调用方式简单。
- • getter:
@property;setter:@属性名.setter;deleter:@属性名.deleter。 - • 适用于:属性访问需要额外逻辑时;希望保持接口一致性时;需要计算属性时。
核心理解:@property 让你在不改变外部调用方式的前提下,优雅地控制属性的访问和修改,是实现封装和数据验证的重要工具。