Skip to content

Python @property:给属性装上"智能门禁"

引言:直接开门 vs 智能门禁

想象你开了一家店,仓库里放着贵重商品(类的内部数据):

  • 直接开门:顾客随便进,想拿啥拿啥——s.score = 9999,乱套了;
  • 智能门禁:顾客按门铃,你检查身份后才开门——既能控制,又不麻烦顾客。

@property 就是这个"智能门禁"——看起来像直接开门,实际每次都有人在背后检查


一、问题起源:属性暴露的隐患

1.1 直接暴露属性的危险

python
class Student(object):
    pass

s = Student()
s.score = 9999   # 成绩改成 9999?程序不报错,但逻辑完全错误

生活化理解:仓库大门敞开,有人把标价 100 的东西改成 10000——系统不拦,但生意没法做了。

1.2 传统方案:getter/setter 方法

python
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

使用:

python
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 基本用法

python
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

使用:

python
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

python
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 自动计算

使用:

python
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 错误示范

python
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 正确做法:内部变量加下划线

python
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,却试图赋值

python
class Student(object):
    @property
    def score(self):
        return self._score

s = Student()
s.score = 60   # ❌ AttributeError: can't set attribute

原因:只写了 getter,没写 setter,这是只读属性。

修正:加上 @score.setter

python
@score.setter
def score(self, value):
    self._score = value

6.2 误区二:setter 里又调用 self.score

python
@score.setter
def score(self, value):
    self.score = value   # ❌ 又触发 setter,无限递归!

修正:操作内部变量 _score

python
@score.setter
def score(self, value):
    self._score = value   # ✅

6.3 误区三:以为 @property 只能用于计算

python
class Student(object):
    @property
    def info(self):
        return f'{self.name} is {self.age} years old'

@property 常用于实时计算(如 age),但也可以用于封装简单字段(如 score)——核心价值是控制访问,不只是计算。

6.4 误区四:在 __init__ 里绕过 setter

python
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:

python
def __init__(self, score):
    self.score = score   # ✅ 触发 setter 验证

七、实际应用案例

案例 1:温度转换器——只读属性的典型场景

python
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:电商订单——金额计算与验证

python
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 分辨率

请利用 @propertyScreen 对象加上 widthheight 属性,以及一个只读属性 resolution(分辨率 = 宽 × 高):

python
class Screen(object):
    pass

# 测试
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('测试通过!')
else:
    print('测试失败!')
参考答案
python
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

九、小结

  1. 问题:直接暴露属性无法验证,用方法又太啰嗦;
  2. 方案@property 把方法变成"像属性一样调用"——简洁 + 可控;
  3. 三件套@property(getter)、@xxx.setter(setter)、只读属性(只写 getter);
  4. 关键规则:内部变量用 _xxx(单下划线),避免与 @property 方法重名导致无限递归;
  5. 适用场景:需要验证的参数、由其他字段计算的字段、敏感数据只读保护;
  6. 核心价值:调用方感觉像直接操作属性,实际每次都有代码在背后把关——简洁与安全的完美平衡

@property 是 Python 面向对象中最优雅的设计之一:它让"严格管控"和"简单易用"这对矛盾体,在语法层面握手言和。