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