第7章 7.2 封装、属性装饰器 @property
🎯 学这个之前,先听我讲个故事
上节课我们给小明写了一个「学生信息卡」程序:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
# 用起来很爽
xiaoming = Student("小明", 85)
print(xiaoming.score) # 85
但是!你有没有想过一个问题:如果有人手滑写了 xiaoming.score = -50 呢?
分数居然能是负数?这不科学!
再比如,你写了一个「银行账户」类:
class BankAccount:
def __init__(self, balance):
self.balance = balance
account = BankAccount(1000)
account.balance = -999999 # 我的账户居然变成负债了?
这就是「封装」要解决的问题 —— 不是把所有东西藏起来,而是「让对的人用对的方式访问数据」。
学完这章,你就能写出「智能的数据访问」:分数自动校验、余额不能乱改、访问有记录……
🧱 基础 25 分钟:封装是什么? @property 怎么用?
1. 封装到底是啥?
说白了:封装就是给数据加「门卫」。
想象你家小区的大门:
- 门禁卡(getter)→ 刷卡看信息
- 密码锁(setter)→ 输入密码才能改信息
- 访客系统 → 记录谁进来了
类比到程序世界:
- 直接访问属性:obj.name → 像敞开的大门,谁都能进
- 用 getter 访问:obj.get_name() → 像门禁,要刷卡
- 用 setter 修改:obj.set_name("新名字") → 像密码锁,要验证
那问题来了:Python 明明可以直接 obj.name,为什么要多此一举?
答案:数据验证 + 逻辑保护。你想限制分数只能在 0-100 之间?用 @property 就能做到。
2. 最简单的例子:把分数锁死
class Student:
def __init__(self, name, score):
self.name = name
self.score = score # 这里会自动调用 setter
@property
def score(self):
"""读取分数时会自动调用这个方法"""
return self._score # 读取的是私有属性 _score
@score.setter
def score(self, value):
"""修改分数时会自动调用这个方法"""
if value < 0:
value = 0 # 不能为负数,自动改成 0
elif value > 100:
value = 100 # 不能超过100,自动改成100
self._score = value # 真正存储的地方是 _score
# 测试一下
xiaoming = Student("小明", 85)
print(xiaoming.score) # 85 ✅
xiaoming.score = 150 # 试着改成150
print(xiaoming.score) # 100 ✅ 自动修正了!
xiaoming.score = -20 # 试着改成负数
print(xiaoming.score) # 0 ✅ 自动修正了!
这3行代码在干嘛:
- @property 装饰器把 self.score 变成「受保护的读操作」
- @xxx.setter 装饰器把 self.score = xxx 变成「受保护的写操作」
- self._score 才是真正存数据的地方(前面加下划线表示私有)
3. 生活类比:为什么需要这个「包装盒」?
想象你在网店买水果:
- 没有封装:你直接伸手进箱子摸 → 可能摸到烂的、不够秤
- 有封装:店家给你装好的盒装水果 → 店家已经挑过、称过、封好了
@property 就是那个「封装好的盒子」:
- 读取时 → 给你的是处理好的值
- 写入时 → 先过一遍检验,合格了才收下

4. 私有属性:单下划线 _ 和双下划线 __
Python 里有个「潜规则」:
class Student:
def __init__(self, name, score):
self.name = name # 公有属性:随便访问
self._score = score # 受保护属性:约定不直接访问
self.__private = 100 # 私有属性:名字被改写了(名字重整)
xiaoming = Student("小明", 85)
print(xiaoming.name) # ✅ 小明 - 公有属性
print(xiaoming._score) # ⚠️ 85 - 能访问,但不建议
print(xiaoming.__private) # ❌ AttributeError! 报错!
print(xiaoming._Student__private) # ✅ 100 - 硬要找也能找到,但不推荐
一句话总结:
- _x = 「兄弟,这个我私用的,你别直接动」
- __x = Python 帮你把名字改了,碰碰运气看能不能找到
5. 只读属性:不给改的机会
有时候数据只能读、不能改,比如「学号」:
class Student:
def __init__(self, name, student_id):
self.name = name
self._student_id = student_id
@property
def student_id(self):
"""学号只能读,不能改"""
return self._student_id
# 测试
xiaoming = Student("小明", "A2024001")
print(xiaoming.student_id) # A2024001
xiaoming.student_id = "A2024999" # ❌ AttributeError!
6. @property 还能干点别的:计算属性
有时候属性不是存着的,而是「算出来」的:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""面积不需要存,每次用的时候算"""
return self.width * self.height
@property
def perimeter(self):
"""周长也不需要存"""
return 2 * (self.width + self.height)
# 测试
rect = Rectangle(10, 5)
print(rect.area) # 50
print(rect.perimeter) # 30
关键点:面积和周长没有存 self.area,而是每次根据宽高「算」出来的!

7. setter 还能做更多:日志记录
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
print(f"[日志] {self.owner} 的账户被修改了!")
print(f"[日志] 旧值: {self._balance if hasattr(self, '_balance') else 'N/A'}, 新值: {value}")
self._balance = value
# 测试
account = BankAccount("小明", 1000)
account.balance = 2000 # 会打印日志!
🔥 实战 35 分钟:3 个递进项目
项目 1(5 分钟):智能学生成绩管理
目标:巩固 @property 的基本用法
class Student:
def __init__(self, name, chinese, math, english):
self.name = name
self.chinese = chinese
self.math = math
self.english = english
@property
def average(self):
"""计算平均分"""
return (self.chinese + self.math + self.english) / 3
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, value):
self._chinese = max(0, min(100, value)) # 自动限制在 0-100
@property
def math(self):
return self._math
@math.setter
def math(self, value):
self._math = max(0, min(100, value))
@property
def english(self):
return self._english
@english.setter
def english(self, value):
self._english = max(0, min(100, value))
# 测试
xiaoming = Student("小明", 85, 90, 78)
print(f"学生: {xiaoming.name}")
print(f"平均分: {xiaoming.average:.1f}")
# 试着输入异常分数
xiaoming.math = 120 # 超出范围
xiaoming.english = -10 # 低于范围
print(f"数学成绩(修正后): {xiaoming.math}") # 应该是 100
print(f"英语成绩(修正后): {xiaoming.english}") # 应该是 0
print(f"平均分(修正后): {xiaoming.average:.1f}")
预期输出:
学生: 小明
平均分: 84.3
数学成绩(修正后): 100
英语成绩(修正后): 0
平均分(修正后): 87.7
一句话解释:@property 自动把越界的分数「拽」回合法范围
项目 2(15 分钟):个人记账本(带数据验证)
目标:综合运用 getter/setter + 计算属性 + 数据验证
import json
from datetime import datetime
class ExpenseRecord:
"""个人记账记录"""
def __init__(self, category, amount, description=""):
self.category = category
self.amount = amount
self.description = description
self.date = datetime.now().strftime("%Y-%m-%d")
@property
def category(self):
return self._category
@category.setter
def category(self, value):
valid_categories = ["餐饮", "交通", "住宿", "购物", "娱乐", "其他"]
if value not in valid_categories:
print(f"⚠️ 警告: 类别 '{value}' 不在预设中,已自动设为 '其他'")
value = "其他"
self._category = value
@property
def amount(self):
return self._amount
@amount.setter
def amount(self, value):
if value < 0:
raise ValueError("金额不能为负数!")
self._amount = round(value, 2) # 保留2位小数
def __str__(self):
return f"[{self.date}] {self.category} - ¥{self.amount:.2f} ({self.description})"
class AccountBook:
"""记账本容器"""
def __init__(self):
self._records = []
def add(self, category, amount, description=""):
"""添加一条记录"""
record = ExpenseRecord(category, amount, description)
self._records.append(record)
return record
@property
def total(self):
"""计算总支出"""
return sum(r.amount for r in self._records)
@property
def by_category(self):
"""按类别分组统计"""
result = {}
for r in self._records:
result[r.category] = result.get(r.category, 0) + r.amount
return result
def show_summary(self):
print("\n" + "=" * 40)
print(f"📊 记账本汇总(共 {len(self._records)} 条记录)")
print("=" * 40)
print(f"💰 总支出: ¥{self.total:.2f}")
print("\n📂 分类明细:")
for cat, amt in self.by_category.items():
print(f" {cat}: ¥{amt:.2f}")
print("=" * 40)
# 测试
book = AccountBook()
# 添加一些记录
book.add("餐饮", 35.5, "午餐")
book.add("交通", 8.0, "地铁")
book.add("购物", 299.9, "日用品")
book.add("娱乐", 88.0, "电影")
book.add("餐饮", 42.0, "晚餐")
book.add("餐饮", 200, "请客吃饭")
book.add("娱乐", 50.0, "无效类别测试") # 会触发警告
try:
book.add("餐饮", -100, "这不应该发生")
except ValueError as e:
print(f"\n✅ 捕获到异常: {e}")
book.show_summary()
# 展示单条记录
print("\n📝 记录明细:")
for record in book._records:
print(f" {record}")
预期输出:
⚠️ 警告: 类别 '娱乐' 不在预设中,已自动设为 '其他'
✅ 捕获到异常: 金额不能为负数!
========================================
📊 记账本汇总(共 6 条记录)
========================================
💰 总支出: ¥723.40
📂 分类明细:
饮: ¥277.50
通: ¥8.00
物: ¥299.90
他: ¥138.00
========================================
📝 记录明细:
2026-06-26] 餐饮 - ¥35.50 (午餐)
..
一句话解释:金额负数直接抛异常,类别不对自动归到「其他」
项目 3(15 分钟):命令行待办清单(文件持久化)
目标:组合多个类 + 属性装饰器 + 文件读写
import json
from pathlib import Path
class TodoItem:
"""单个待办事项"""
def __init__(self, title, priority=3):
self.title = title
self._priority = priority
self._done = False
@property
def priority(self):
return self._priority
@priority.setter
def priority(self, value):
if not 1 <= value <= 5:
raise ValueError("优先级必须在 1-5 之间")
self._priority = value
@property
def done(self):
return self._done
@done.setter
def done(self, value):
self._done = bool(value)
def __str__(self):
status = "✅" if self._done else "⬜"
stars = "⭐" * self._priority
return f"{status} {self.title} {stars}"
def to_dict(self):
return {"title": self.title, "priority": self._priority, "done": self._done}
@classmethod
def from_dict(cls, data):
item = cls(data["title"], data["priority"])
item._done = data["done"]
return item
class TodoList:
"""待办清单管理器"""
def __init__(self, filename="todo_data.json"):
self._filename = filename
self._items = []
self._load()
def _load(self):
"""从文件加载数据"""
if Path(self._filename).exists():
with open(self._filename, "r", encoding="utf-8") as f:
data = json.load(f)
self._items = [TodoItem.from_dict(d) for d in data]
def _save(self):
"""保存数据到文件"""
data = [item.to_dict() for item in self._items]
with open(self._filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def add(self, title, priority=3):
"""添加待办"""
item = TodoItem(title, priority)
self._items.append(item)
self._save()
print(f"✅ 已添加: {item}")
def done(self, index):
"""标记为完成"""
if 0 <= index < len(self._items):
self._items[index].done = True
self._save()
print(f"✅ 已完成: {self._items[index]}")
else:
print(f"❌ 无效序号: {index}")
def delete(self, index):
"""删除待办"""
if 0 <= index < len(self._items):
removed = self._items.pop(index)
self._save()
print(f"🗑️ 已删除: {removed}")
else:
print(f"❌ 无效序号: {index}")
def show(self):
"""显示所有待办"""
if not self._items:
print("📝 清单是空的!")
return
pending = [item for item in self._items if not item.done]
completed = [item for item in self._items if item.done]
print(f"\n📋 待办事项(共 {len(pending)} 条未完成):")
for i, item in enumerate(pending):
print(f" {i}. {item}")
if completed:
print(f"\n✅ 已完成(共 {len(completed)} 条):")
for i, item in enumerate(completed, len(pending)):
print(f" {i}. {item}")
def clear_done(self):
"""清空已完成"""
self._items = [item for item in self._items if not item.done]
self._save()
print("🧹 已清空所有已完成项目")
# 演示
if __name__ == "__main__":
todo = TodoList()
# 演示命令
print("=== 添加待办 ===")
todo.add("完成 Python 作业", priority=5)
todo.add("给妈妈打电话", priority=4)
todo.add("整理房间", priority=2)
todo.add("invalid priority test", priority=10) # 会报错
print("\n=== 显示清单 ===")
todo.show()
print("\n=== 标记完成 ===")
todo.done(1)
print("\n=== 最终清单 ===")
todo.show()
预期输出:
=== 添加待办 ===
✅ 已添加: ⬜ 完成 Python 作业 ⭐⭐⭐⭐⭐
✅ 已添加: ⬜ 给妈妈打电话 ⭐⭐⭐⭐
✅ 已添加: ⬜ 整理房间 ⭐⭐
❌ 优先级必须在 1-5 之间
=== 显示清单 ===
📋 待办事项(共 3 条未完成):
0. ⬜ 完成 Python 作业 ⭐⭐⭐⭐⭐
1. ⬜ 给妈妈打电话 ⭐⭐⭐⭐
2. ⬜ 整理房间 ⭐⭐
=== 标记完成 ===
✅ 已完成: ⬜ 给妈妈打电话 ⭐⭐⭐⭐
=== 最终清单 ===
📋 待办事项(共 2 条未完成):
0. ⬜ 完成 Python 作业 ⭐⭐⭐⭐⭐
1. ⬜ 整理房间 ⭐⭐
✅ 已完成(共 1 条):
2. ✅ 给妈妈打电话 ⭐⭐⭐⭐
一句话解释:数据会自动保存到 JSON 文件,关闭程序再打开,数据还在
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:property 和实例属性的「名字冲突」
# ❌ 错误示例
class Student:
def __init__(self, score):
self.score = score # 这里触发了 setter,但此时 self.score 还不存在!
@property
def score(self):
return self._score
@score.setter
def score(self, value):
print("正在设置分数")
self._score = value
# ✅ 正确示例
class Student:
def __init__(self, score):
self.score = score # 没问题!setter 已经在类定义时创建好了
@property
def score(self):
return self._score
@score.setter
def score(self, value):
print("正在设置分数")
self._score = value
坑 2:不要在 setter 里调用同一个 property
# ❌ 错误示例 - 无限递归!
class Student:
@property
def score(self):
return self.score # 递归调用自己!
@score.setter
def score(self, value):
self.score = value # 递归调用 setter!
坑 3:property 会「覆盖」子类同名属性
# ⚠️ 小心这个陷阱
class Parent:
@property
def name(self):
return "Parent"
class Child(Parent):
pass
print(Child().name) # "Parent" - 子类没有自己的 name!
坑 4:__slots__ 和 property 的配合
# 使用 __slots__ 可以节省内存,但要注意写法
class Student:
__slots__ = ("_name", "_score")
def __init__(self, name, score):
self.name = name
self.score = score
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def score(self):
return self._score
@score.setter
def score(self, value):
self._score = value
# ✅ 正常用
s = Student("小明", 85)
print(s.name, s.score)
坑 5:property 不是真正的「限制」
# ⚠️ 重要提醒:property 只是约定,不是硬限制
class Student:
def __init__(self, score):
self.score = score
@property
def score(self):
return self._score
@score.setter
def score(self, value):
self._score = max(0, min(100, value))
s = Student(85)
s.score = 50 # ✅ 通过 property 修改
s._score = 999 # ⚠️ 绕过了限制!直接访问私有属性
print(s.score) # 999 - 限制失效了
一句话总结:property 是「防君子不防小人」的约定,主要目的是让代码更清晰、逻辑更安全,而不是硬加密
调试技巧:用 __repr__ 看清楚对象状态
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __repr__(self):
return f"Student(name='{self.name}', score={self.score})"
s = Student("小明", 85)
print(repr(s)) # Student(name='小明', score=85)
✏️ 练习题
练习 1(2 分钟):添加一个验证
# 输入:在 BankAccount 类中添加一个 minimum 属性,保证余额不低于这个数
# 预期输出:设置 minimum 后,取款不能使余额低于 minimum
练习 2(2 分钟):在项目 1 里加判断
# 输入:给 Student 类加一个 @property,输出"及格"或"不及格"
# 预期输出:平均分<60输出"不及格",否则"及格"
练习 3(3 分钟):处理新数据
# 输入:用项目 2 的记账本统计一份新的消费记录
# 提示:直接把 add() 的调用换成新数据
练习 4(3 分钟):串接两个项目
# 输入:用项目 2 的 ExpenseRecord 类替代项目 1 里的简单属性
# 预期输出:每科成绩有分类(测验/期中/期末),分类汇总显示
练习 5(2 分钟):看图找错
# 下面这段代码运行后会报错,找出问题所在
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("价格不能为负")
self._price = value
p = Product("苹果", -5)
print(p.price)
作业:做一个「个人健康数据记录器」
需求描述:记录每天的体重、运动时长、喝水量等健康数据,带统计和分析功能。
功能点:
1. 记录每天的数据(日期、体重、运动时长、喝水量)
2. 所有数值都有验证(体重 20-300kg,运动 0-24小时,喝水量 0-10L)
3. 计算周平均值、显示趋势(体重上升/下降箭头)
4. 数据保存到本地 JSON 文件
加分项:
1. BMI 计算(自动根据身高体重算)
2. 目标达成提醒(比如"今天运动量达标")
验收标准:
- 能运行不报错
- 输入异常数据能正确处理
- 统计结果准确
📚 总结
本文学了 3 件事:
1. 封装:用 _x 或 __x 表示私有属性,给数据加门卫
2. @property:把方法变成「像属性一样访问」,读写时自动校验
3. setter 验证:修改数据前先过一遍检查,保证数据合法
延伸资源:
- 官方文档:https://docs.python.org/3/library/functions.html#property
- 《Python 编程:从入门到实践》第 9 章 - 类
- 视频:B 站「小甲鱼」Python 教程第 38 讲
互动钩子:你在做项目时有没有遇到过「明明不该修改的数据被人改乱了」的情况?评论区聊聊你是怎么解决的!老粉优先回复哦~
下章剧透:学会了封装,你的类就像一个个「独立的房间」……但如果两个房间有「共同之处」呢?比如「学生」和「老师」都是「人」?下一章我们来聊继承——让代码复用的魔法!

评论(0)