Python @property 装饰器教程

为什么需要“属性装个门”

直接在类里暴露属性,就像把家门钥匙插在门外——谁都能进来随便改,完全不受控制。

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

s = Student()
s.score = 9999  # 满分750的高考,这分数太离谱了

这种“裸奔”的设计,会让非法数据(比如负数年龄、超大分数)随意污染你的程序,排查起来很痛苦。

传统的解决办法是用 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

安全归安全,但每次读写都要写成 obj.get_score()obj.set_score(85),总觉得别扭——我们明明是在操作一个“属性”,却被迫用方法调用的形式,一点不 Pythonic。

Python 提供了 @property 装饰器,让方法长得像普通属性一样,既保留封装检查,又保持调用的简洁优雅。


@property 的核心魔法

@property 的核心用途:把“读”方法变成一个属性访问,把“写”方法变成赋值操作

标准写法结构

class 某个类:
    # 1. 读取属性——用 @property 装起来
    @property
    def 属性名(self):
        # 可以加任何访问逻辑(比如格式化、日志)
        return self._内部存储变量
    
    # 2. 修改属性——用 @属性名.setter 装起来
    @属性名.setter
    def 属性名(self, value):
        # 可以加验证、类型转换等
        self._内部存储变量 = value

完整实战:学生分数类

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 = 85       # ✅ 自动调用 @score.setter 里的验证逻辑
print(s.score)     # ✅ 自动调用 @property 里的读取逻辑
# 85

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

看到没?表面上我们在操作 score 这个“属性”,背后却悄悄运行着一整套安全验证。


搞一个只读的“计算属性”

如果你只定义 @property,而不定义对应的 setter,那这个属性就成了只读属性——不能被赋值,特别适合做根据其他属性实时计算出来的值,比如根据出生年份算年龄、根据宽高算面积。

示例:可以改出生年,但年龄只读

class Student:
    def __init__(self, birth_year):
        self._birth = birth_year

    # 可修改的出生年份
    @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

    # 只读的计算属性——年龄
    @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 = 20      # ❌ 报错:AttributeError: can't set attribute

这样一来,我们可以保证 age 永远是由 birth 准确计算出来的,外部不可能随意篡改年龄值。


几个新手最容易踩的坑

1. 属性名和内部变量名一样 → 无限递归

如果你把 @property 装饰的方法名,跟内部真正存放数据的变量名设置成一样,会发生恐怖的死循环。

# ❌ 致命的无限递归
class Student:
    @property
    def birth(self):
        return self.birth   # 又去读 self.birth,又触发这个 property……

正确做法:内部存储变量统一用单下划线前缀(如 _birth),这是 Python 社区的约定,表示“这个变量是内部用的,外人别乱碰”。

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

使用 @property 之前,一定要在 __init__ 等方法里把对应的内部变量(_xxx)创建出来,否则访问时会找不到属性。

# ❌ 忘记创建 _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

    # 宽度属性(合法值必须是正数)
    @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

测试效果

screen = Screen()                           # 默认1080p
print(f"默认分辨率: {screen.resolution}")    # 2073600

screen.width = 3840
screen.height = 2160
print(f"4K分辨率: {screen.resolution}")     # 8294400

screen.width = -1024   # ❌ 抛出 ValueError,完美拦截

什么时候该用,什么时候大胆裸奔

  • 建议用 @property 的场景

    1. 赋值时需要校验或转换(如年龄≥0,字符串不能为空)
    2. 需要动态计算只读值(面积、总价、年龄)
    3. 项目后期想给本来直接暴露的属性加上控制,又不想改变外部调用代码
  • 可以直接用普通属性的场景
    不需要任何额外逻辑,只是单纯存、取数据,Python 鼓励“我们都值得信任”,直接暴露即可,代码反而更简单。


小结

@property 装饰器在“裸露”与“繁琐”之间找到了最佳平衡点:它让你以操作普通属性的方式,享受完整的封装保护。学会它,你的 Python 类接口会变得既安全又优雅,一看就是老司机的风格~