Python @property 装饰器教程

为什么需要属性装饰器

在面向对象编程中,直接暴露类的内部属性虽然能快速上手,但会完全失去对属性赋值与访问的控制权:比如分数可以设成负数、超高分,或者日期设成未来的格式,这些非法状态都会让程序变得不可靠。

# ❌ 危险的直接暴露属性
class Student:
    pass

s = Student()
s.score = 9999  # 高考满分都不会这么高,完全不合规

为了填补这个漏洞,传统做法是使用 Java/C++ 风格的 getter/setter 方法

# ✅ 但调用不够优雅的传统封装
class Student:
    def __init__(self):
        self._score = 0  # 用单下划线表示“内部属性”,不建议外部直接访问

    def get_score(self):
        return self._score
    
    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('分数必须是整数!')
        if not (0 <= value <= 100):
            raise ValueError('分数必须在0~100之间!')
        self._score = value

但这样的调用太啰嗦了——明明是像“属性”一样的东西,却要写括号调用方法。Python 作为追求可读性与优雅性的语言,提供了更贴合习惯的方案:@property 装饰器。


@property 的基本用法

@property 的核心作用是把一个方法伪装成“属性”,同时保留封装的所有优势。

核心结构

class 类名:
    # 1. 定义「访问属性」的方法,加 @property 装饰器
    @property
    def 属性名(self):
        # 这里可以加访问前的验证、格式化逻辑
        return self._内部存储变量
    
    # 2. 定义「修改属性」的方法,加 @属性名.setter 装饰器
    @属性名.setter
    def 属性名(self, value):
        # 这里可以加修改前的验证、转换逻辑
        self._内部存储变量 = value

完整示例(对应上面的 Student 类)

# ✅ 简洁又安全的 Python 风格封装
class Student:
    def __init__(self):
        self._score = 0

    @property
    def score(self):
        return self._score
    
    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('分数必须是整数!')
        if not (0 <= value <= 100):
            raise ValueError('分数必须在0~100之间!')
        self._score = value

日常调用

完全和操作普通属性一样!

s = Student()

s.score = 60  # ✅ 自动触发 @score.setter 对应的方法
print(s.score)  # ✅ 自动触发 @property 对应的方法

s.score = 9999  # ❌ 自动抛出 ValueError,完美拦截非法值

只读属性

如果只加 @property,不加对应的 @属性名.setter,这个属性就会变成只读属性——只能获取,不能修改,非常适合用来做「计算属性」(比如根据生日算年龄、根据宽高算分辨率)。

示例:带计算属性的 Student 类

class Student:
    def __init__(self, birth_year):
        self._birth = birth_year  # 初始化内部存储的出生年份

    # 可以修改的属性(有 setter)
    @property
    def birth(self):
        return self._birth
    
    @birth.setter
    def birth(self, value):
        # 可以加简单的年份验证
        if not isinstance(value, int) or value > 2024:
            raise ValueError('请输入合法的出生年份!')
        self._birth = value

    # 只读的计算属性(没有 setter)
    @property
    def age(self):
        return 2024 - self._birth

测试只读属性

s = Student(2000)

print(s.birth)  # ✅ 2000
print(s.age)    # ✅ 24
s.birth = 2005  # ✅ 合法修改
print(s.age)    # ✅ 自动更新为19

s.age = 23      # ❌ 自动抛出 AttributeError:can't set attribute

几个容易踩的坑

1. 属性名与内部变量名冲突 → 无限递归

如果把 @property 装饰的方法名和内部存储变量名完全一样,访问时会陷入“调用方法 → 访问同名变量 → 再次调用方法”的死循环。

# ❌ 致命的无限递归
class Student:
    @property
    def birth(self):
        return self.birth  # 这里访问 self.birth 又会触发 @property 装饰的 birth() 方法

正确做法:用单下划线前缀命名内部存储变量(Python 约定俗成的“内部私有属性”标识,不是强制语法,但外部直接访问会显得不专业)。

2. 忘记初始化内部变量 → AttributeError

如果不在 __init__ 或其他前置方法中初始化带单下划线的内部变量,第一次访问属性时会报错找不到变量。

# ❌ 忘记初始化 _score
class Student:
    @property
    def score(self):
        return self._score

s = Student()
print(s.score)  # ❌ AttributeError:'Student' object has no attribute '_score'

实际应用示例:屏幕分辨率类

结合只读计算属性和合法值验证,写一个实用的 Screen 类:

class Screen:
    def __init__(self, width=1920, height=1080):
        self._width = width
        self._height = height

    # 合法宽度必须是正数(int/float 均可)
    @property
    def width(self):
        return self._width
    
    @width.setter
    def width(self, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError('宽度必须是大于0的数值!')
        self._width = value

    # 合法高度同上
    @property
    def height(self):
        return self._height
    
    @height.setter
    def height(self, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError('高度必须是大于0的数值!')
        self._height = value

    # 只读的计算属性:分辨率
    @property
    def resolution(self):
        return self._width * self._height

测试这个类

# 初始化默认1080P屏幕
screen = Screen()
print(f"默认分辨率: {screen.resolution}")  # ✅ 2073600

# 修改为4K屏幕
screen.width = 3840
screen.height = 2160
print(f"4K分辨率: {screen.resolution}")  # ✅ 8294400

# 尝试设置非法分辨率
screen.width = -1024  # ❌ ValueError

最佳实践总结

  1. 什么时候用 @property?
    • 需要对属性赋值做验证/转换(比如年龄必须≥0,名字必须是字符串)
    • 需要创建只读的计算属性(比如根据宽高算面积)
    • 需要向后兼容:原本是直接暴露属性,后来想加封装,用 @property 可以不用改外部调用代码
  2. 什么时候别用?
    • 只是简单的赋值/获取,没有额外逻辑——直接暴露属性就行(Python 强调“我们都是成年人了,不乱改内部属性”)
  3. 命名规范要遵守
    • 内部存储变量用单下划线前缀_score
    • 暴露的属性名直接用直观的名称score

最后

@property 装饰器完美平衡了面向对象的封装性Python 的简洁性,既能保护类的内部状态,又不会让调用代码变得繁琐。掌握它,是写出优雅 Python 类接口的第一步~