面向对象三大特性:封装、继承、多态。封装的核心是隐藏内部数据,对外提供受控的访问接口。
在 Python 中,实现封装最常见的武器就是@property装饰器。它让你既能像访问普通属性一样自然(obj.attr),又能像方法一样实现校验、计算甚至只读控制。
很多人初学时觉得@property只是 “getter/setter 的语法糖”,但其实它的设计哲学远比语法糖深刻 —— 它是Python对“属性访问一致性”的最佳实践。
2. @property基础语法(标准模板+完整调用示例)
python class Person: def __init__(self, age): self._age = age #习惯用单下划线表示“受保护”
@property def age(self): # getter """我是获取年龄的接口""" return self._age
@age.setter def age(self, new_age): # setter """我是设置年龄的接口,带校验""" if new_age < 0: raise ValueError("年龄不能为负数") self._age = new_age
@age.deleter def age(self): # deleter(可选) """删除年龄属性""" del self._age
# ========== 调用示例 ========== if __name__ == "__main__": p = Person(18)
# 触发 getter 获取属性 print(p.age) # 输出:18
# 触发 setter 修改属性 p.age = 22 print(p.age) # 输出:22
# 触发 setter,传入非法值,抛出异常 try: p.age = -5 except ValueError as e: print(e) # 输出:年龄不能为负数
# 触发 deleter 删除属性 del p.age # print(p.age) # 删除后访问会报 AttributeError |
2.1三个装饰器的书写要求
装饰器 | 位置 | 方法名一致性 | 参数要求 |
@property | 必须写在 getter 上方 | 固定写法,后无内容 | 仅self |
@age.setter | 写在 setter 上方 | 必须与getter方法名完全一致(这里是age) | self+ 一个接收新值的参数 |
@age.deleter | 写在 deleter 上方 | 必须与getter方法名完全一致(这里是age) | 仅self |
⚠️命名致命伤:如果getter命名为def age,setter却写成@Age.setter,会报AttributeError。 |
3.三种实现方式对比(直击痛点)
方式 | 外部调用 | 能否校验 | 代码量 | 重构友好度 | Python 风格 |
直接暴露公有属性 | obj.age = 20 | ❌ | 极少 | 极差(改后外部全崩) | ✅ 简洁但不安全 |
传统get/set方法 | obj.set_age(20) | ✅ | 极多 | 一般(外部需改动) | ❌ 啰嗦且非主流 |
@property | obj.age = 20 | ✅ | 适中 | 极佳(外部无需改动) | ✅ 简洁安全 |
4.为什么要用@property?(核心价值)
4.1向后兼容性(接口不变性)
这是@property最伟大的设计。想象一个项目初期:
python class User: def __init__(self, name): self.name = name #直接暴露 |
到处都在用u.name = "Tom"。后来需求变更,要求name必须首字母大写。
如果用传统方式,你只能改成set_name(),牵一发而动全身。
但用@property,你只需悄然升级类内部:
python class User: def __init__(self, name): self._name = name
@property def name(self): return self._name
@name.setter def name(self, value): self._name = value.capitalize() #添加逻辑
# 调用示例 if __name__ == "__main__": u = User("tom") u.name = "lily" print(u.name) # 输出:Lily,自动首字母大写,外部代码无需修改 |
外部代码u.name = "tom"一行不用改,却自动转成了"Tom"。
这就是“在不破坏接口的前提下增强功能”,是@property的灵魂。
4.2实现只读属性(不写setter)
python class Circle: def __init__(self, radius): self._radius = radius
@property def area(self): #只有 getter,没有 setter return 3.14 * self._radius ** 2
# 调用示例 if __name__ == "__main__": c = Circle(10) print(c.area) # 输出:314.0 # c.area = 100 # 取消注释,报 AttributeError: can't set attribute,只读生效 |
外部执行c.area = 100会报错AttributeError: can't set attribute,天然实现了只读。
4.3懒加载(延迟计算)
python class DataLoader: def _load_from_db(self): """模拟耗时数据库加载""" print("正在从数据库加载大量数据...") return [1, 2, 3, 4, 5]
@property def huge_data(self): if not hasattr(self, '_huge_data'): self._huge_data = self._load_from_db() # 首次访问才加载 return self._huge_data
# 调用示例 if __name__ == "__main__": dl = DataLoader() print("对象创建完成,尚未加载数据") print(dl.huge_data) # 第一次访问:执行加载,打印加载日志 print(dl.huge_data) # 第二次访问:直接读取缓存,不再加载 |
5.调用方式与习惯(直观必看)
5.1定义时的习惯命名
- 私有变量用_age或__age(双下划线会名称修饰,更隐蔽,但通常_age足够)。
- setter 的参数名建议value或new_val。
5.2外部调用规范(加粗提醒)
python p = Person(18)
# ✅正确:直接通过属性名访问(无括号) print(p.age) # 触发 getter p.age = 25 # 触发 setter del p.age # 触发 deleter
# ❌ 错误:当成方法调用(加括号) # p.age() # TypeError: 'int' object is not callable |
黄金法则:@property让方法看起来像数据,所以调用时一律不加括号。
5.3内部使用的注意事项
python class Demo: def __init__(self, score): self._score = score
@property def score(self): print("执行getter") return self._score
def show(self): # ✅ 推荐:直接访问底层私有变量,不会触发getter print(self._score)
# ⚠️ 不推荐:会再次触发 score 的 getter # print(self.score)
if __name__ == "__main__": d = Demo(90) d.show() |
在类内部方法中,建议始终通过self._age访问真实数据,而不要用self.age(会再次触发 getter,造成循环或性能损耗)。只有外部才用self.age。
6.常见错误案例分析
错误示范代码:
python class类名: def __init__(self, x): self.__age = x
@property def age(self): return self.__age
@age.setter def age(self, newVal): if newVal > 0 and self.__age == 0: self.__age = 0 # ❌ 这里写死 0,而不是 newVal
@age.deleter def age(self): del self.__age
# 复现bug调用示例 if __name__ == "__main__": obj = 类名(10) obj.age = 20 print(obj.age) # 输出依旧是10,setter没有生效 |
6.1错误分析
- 逻辑Bug:setter 里self.__age = 0,即便满足条件也只会赋 0,而不会赋newVal。
执行对象a.age = 20后,因为初始__age为 10,self.__age == 0为False,条件不满足,setter 什么也不做,所以print(对象a.age)仍然是 10。
✅ 修正后代码:
python @age.setter def age(self, newVal): if newVal > 0: #只做合理性校验 self.__age = newVal # ✅ 赋新值 else: raise ValueError("年龄必须大于0") |
6.2其他易错点
- 忘记写setter:导致属性只读,赋值会报错。
- setter中忘记给私有属性赋值:造成属性 “伪修改”。
- getter中返回了可变对象(如列表),外部直接修改列表元素会绕过 setter,需要返回副本。
python class Student: def __init__(self): self._scores = [80, 90]
@property def scores(self): return self._scores # ❌返回原始列表,外部可篡改
if __name__ == "__main__": s = Student() s.scores.append(100) print(s._scores) # 内部数据被悄悄修改,setter完全没有触发
# ✅ 修复:返回副本 # return self._scores.copy() |
7.进阶:@property与描述符的取舍
当你发现多个类或多个属性需要相同的校验逻辑(如 “非负整数”),@property会产生大量重复代码。此时应升级为自定义描述符(Descriptor):
python class NonNegative: def __set_name__(self, owner, name): self.name = name
def __get__(self, obj, objtype=None): return obj.__dict__.get(self.name, 0)
def __set__(self, obj, value): if value < 0: raise ValueError(f"{self.name} must be >= 0") obj.__dict__[self.name] = value
class Order: quantity = NonNegative() price = NonNegative()
#调用示例 if __name__ == "__main__": o = Order() o.quantity = 10 o.price = 99 print(o.quantity, o.price) # o.price = -10 # 抛异常,校验复用生效 |
描述符可以多处复用,比单独写多个@property更干净。
8.总结:一张图记住@property
Plain Text +----------------+ +-------------------+ |外部调用 | | 类内部实际执行 | | obj.age = 20 | =====> | @age.setter | | print(obj.age)| =====> | @property getter | | del obj.age | =====> | @age.deleter | +----------------+ +-------------------+ |
- 替代方案:传统 get/set(不推荐)、描述符(高级复用)。
- 核心原则:外部像属性,内部像方法,兼顾安全与优雅。
💡忠告:不要为了用而用——如果属性真的不需要任何控制,直接暴露self.age也是可以的,但一旦后期需要增加逻辑,一定要第一时间重构为@property,这是Python开发者成熟的标志。 |
本文由“水哥澎湃”学习笔记整理而成,欢迎转载,请注明出处