Python @property:给属性装上"智能门禁"
引言:直接开门 vs 智能门禁
想象你开了一家店,仓库里放着贵重商品(类的内部数据):
- 直接开门:顾客随便进,想拿啥拿啥——
s.score = 9999,乱套了; - 智能门禁:顾客按门铃,你检查身份后才开门——既能控制,又不麻烦顾客。
@property 就是这个"智能门禁"——看起来像直接开门,实际每次都有人在背后检查。
一、问题起源:属性暴露的隐患
1.1 直接暴露属性的危险
class Student(object):
pass
s = Student()
s.score = 9999 # 成绩改成 9999?程序不报错,但逻辑完全错误生活化理解:仓库大门敞开,有人把标价 100 的东西改成 10000——系统不拦,但生意没法做了。
1.2 传统方案:getter/setter 方法
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value使用:
s = Student()
s.set_score(60) # ✅ 正常
print(s.get_score()) # 60
s.set_score(9999) # ❌ ValueError: score must between 0 ~ 100!问题:每次都要写 set_score() / get_score(),太啰嗦,不像属性那样直观。
二、@property:鱼与熊掌兼得
2.1 基本用法
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value使用:
s = Student()
s.score = 60 # ✅ 像属性一样赋值,实际调用 setter
print(s.score) # 60,像属性一样读取,实际调用 getter
s.score = 9999 # ❌ ValueError,被拦截了!生活化理解:顾客按门铃(s.score = 60),门禁系统自动检查(setter 里的验证),没问题才开门。顾客感觉像直接开门,实际背后有全套安检。
2.2 装饰器背后的原理(简化版)
@property 装饰 score() 方法
↓
创建另一个装饰器 @score.setter
↓
s.score = 60 → 自动转成 s.score(60) 调用 setter
s.score → 自动转成 s.score() 调用 getter你写 s.score = 60,Python 偷偷帮你调用了 set_score 逻辑——语法简洁,功能完整。
三、只读属性:只给看,不给改
3.1 定义方式:只写 getter,不写 setter
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2025 - self._birth # 根据 birth 自动计算使用:
s = Student()
s.birth = 2000 # ✅ 可写
print(s.age) # 25,自动算出来
s.age = 30 # ❌ AttributeError: can't set attribute生活化理解:birth 是出生日期登记处(可改),age 是自动计算的年龄显示牌(只读)——你改不了年龄,只能改出生日期,年龄自动跟着变。
3.2 为什么需要只读属性?
| 场景 | 原因 |
|---|---|
| 数据由其他字段计算得出 | 如 age = 当前年 - birth,手动改 age 会导致数据不一致 |
| 敏感数据只供查看 | 如订单号、创建时间,防止误修改 |
| 简化接口 | 调用方不需要知道这是计算出来的,直接用就行 |
四、致命陷阱:方法名与实例变量重名
4.1 错误示范
class Student(object):
@property
def birth(self):
return self.birth # ❌ 危险!发生了什么?
s.birth → 调用 birth() 方法
↓
return self.birth → 又访问 s.birth
↓
又调用 birth() 方法
↓
无限递归 → RecursionError: maximum recursion depth exceeded生活化理解:你问前台"我的房间号是多少",前台说"请报房间号查询"——你报不出,前台再问,死循环了。
4.2 正确做法:内部变量加下划线
class Student(object):
@property
def birth(self):
return self._birth # ✅ 用 _birth 存实际数据
@birth.setter
def birth(self, value):
self._birth = value规则:@property 方法名用 birth,内部存数据的变量用 _birth(单下划线开头,表示"内部使用")。
五、知识链条:从属性暴露到 @property
直接暴露属性(s.score = 9999)
↓
问题:无法验证数据,逻辑错误
↓
传统方案:get_score() / set_score() 方法
↓
问题:调用啰嗦,不像属性直观
↓
解决方案:@property 装饰器
↓
getter 变属性读取 + setter 变属性赋值
↓
扩展:只读属性(只写 getter)
↓
注意:方法名 ≠ 实例变量名(用 _xxx 区分)六、常见误区与避坑指南
6.1 误区一:忘记写 setter,却试图赋值
class Student(object):
@property
def score(self):
return self._score
s = Student()
s.score = 60 # ❌ AttributeError: can't set attribute原因:只写了 getter,没写 setter,这是只读属性。
修正:加上 @score.setter:
@score.setter
def score(self, value):
self._score = value6.2 误区二:setter 里又调用 self.score
@score.setter
def score(self, value):
self.score = value # ❌ 又触发 setter,无限递归!修正:操作内部变量 _score:
@score.setter
def score(self, value):
self._score = value # ✅6.3 误区三:以为 @property 只能用于计算
class Student(object):
@property
def info(self):
return f'{self.name} is {self.age} years old'@property 常用于实时计算(如 age),但也可以用于封装简单字段(如 score)——核心价值是控制访问,不只是计算。
6.4 误区四:在 __init__ 里绕过 setter
class Student(object):
def __init__(self, score):
self._score = score # ❌ 直接操作内部变量,绕过验证
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if value < 0 or value > 100:
raise ValueError('invalid')
self._score = value
s = Student(9999) # 成功!验证失效了修正:__init__ 里也要走 setter:
def __init__(self, score):
self.score = score # ✅ 触发 setter 验证七、实际应用案例
案例 1:温度转换器——只读属性的典型场景
class Temperature(object):
def __init__(self, celsius):
self.celsius = celsius # 触发 setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('温度不能低于绝对零度!')
self._celsius = value
@property
def fahrenheit(self):
"""华氏度,只读,自动计算"""
return self._celsius * 9 / 5 + 32
# 使用
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0,自动转换
t.celsius = 30 # ✅ 修改摄氏度
print(t.fahrenheit) # 86.0,华氏度自动更新
t.fahrenheit = 100 # ❌ AttributeError,只读!生活化理解:温度计上摄氏度是主刻度(可调),华氏度是副刻度(自动换算显示)——你调不了副刻度,只能调主刻度,副刻度跟着变。
案例 2:电商订单——金额计算与验证
class Order(object):
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError('价格不能为负数!')
self._price = value
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError('数量必须是正整数!')
self._quantity = value
@property
def total(self):
"""订单总额,只读,自动计算"""
return self._price * self._quantity
@property
def discount_total(self):
"""满100打9折,只读"""
if self.total >= 100:
return self.total * 0.9
return self.total
# 使用
order = Order(50, 3)
print(order.total) # 150
print(order.discount_total) # 135.0,自动打折
order.quantity = 1 # 修改数量
print(order.total) # 50,自动重算
print(order.discount_total) # 50,不满足打折条件
order.price = -10 # ❌ ValueError: 价格不能为负数!生活化理解:收银台自动算总价和折扣——你只输入单价和数量,总额和折后价自动显示,改任何一个数字,结果立刻刷新。
八、实战练习
练习:Screen 分辨率
请利用 @property 给 Screen 对象加上 width 和 height 属性,以及一个只读属性 resolution(分辨率 = 宽 × 高):
class Screen(object):
pass
# 测试
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
print('测试通过!')
else:
print('测试失败!')参考答案
class Screen(object):
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('宽度必须为正数!')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('高度必须为正数!')
self._height = value
@property
def resolution(self):
"""分辨率 = 宽 × 高,只读"""
return self._width * self._height九、小结
- 问题:直接暴露属性无法验证,用方法又太啰嗦;
- 方案:
@property把方法变成"像属性一样调用"——简洁 + 可控; - 三件套:
@property(getter)、@xxx.setter(setter)、只读属性(只写 getter); - 关键规则:内部变量用
_xxx(单下划线),避免与@property方法重名导致无限递归; - 适用场景:需要验证的参数、由其他字段计算的字段、敏感数据只读保护;
- 核心价值:调用方感觉像直接操作属性,实际每次都有代码在背后把关——简洁与安全的完美平衡。
@property 是 Python 面向对象中最优雅的设计之一:它让"严格管控"和"简单易用"这对矛盾体,在语法层面握手言和。