零基础学 Python 第17章 | 封装与 property:保护数据安全
本章摘要
- 核心目标:掌握封装机制,能用私有属性和
property装饰器保护类内数据安全 - 核心知识点:私有属性(__前缀)、命名重整、
property装饰器、getter/setter、只读属性、数据验证 - 实操产出:银行账户类,通过
property控制存取款并验证金额合法性 - 前置基础:第16章继承、第15章类与对象
写在前面
上一章我们学了继承,子类可以复用父类的属性和方法,代码复用能力大幅提升。但你有没有注意到一个问题——类的属性可以被外部随意修改?
比如你定义了一个 Student 类,里面有个 age 属性。创建对象后,别人随手一句 stu.age = -100,年龄就变成负数了。余额可以被设成负数、成绩可以超过 100 分、用户名可以设成空字符串——这些"脏数据"毫无阻拦地进入你的程序,迟早要出问题。
本章就来解决这个问题。Python 提供了一套机制叫封装:把敏感数据藏起来,只通过受控的方法访问和修改。你将学会用私有属性保护数据、用 property 装饰器控制读写权限、在赋值时做数据验证——这是写出健壮、安全代码的关键一步。
一、为什么需要封装
先看一个"裸奔"的类:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student("小明", 18)
print(stu.age) # 18
# 问题来了——外部可以随意篡改
stu.age = -100
print(stu.age) # -100(荒谬!)
age 是公开属性,任何拿到 stu 对象的人都能直接赋值,写什么都行。负数、字符串、None——Python 不会拦你。
封装的核心思路:把不该被随意修改的数据藏起来,只暴露安全的访问通道。外部想读数据?走 getter。想写数据?走 setter,我顺便检查一下你写的值合不合法。
二、私有属性:双下划线前缀
Python 中,在属性名前面加两个下划线 __,就变成了私有属性:
class Student:
def __init__(self, name, age):
self.name = name # 公开属性
self.__age = age # 私有属性
stu = Student("小明", 18)
# 公开属性可以正常访问
print(stu.name) # 小明
# 私有属性无法直接访问
print(stu.__age) # ❌ AttributeError: 'Student' object has no attribute '__age'
加了 __ 前缀后,外部代码无法通过 stu.__age 直接访问,也无法直接修改。数据被"锁"起来了。
结论:双下划线前缀 = 私有属性,外部不能直接读写。
2.1 通过方法间接访问
属性私有了,但总得有办法读和写吧?通常的做法是提供公开的方法来间接操作:
class Student:
def __init__(self, name, age):
self.name = name
self.__age = age
# getter:读取私有属性
def get_age(self):
return self.__age
# setter:修改私有属性(带验证)
def set_age(self, age):
if age < 0 or age > 150:
print("⚠️ 年龄不合法,必须在 0-150 之间")
return
self.__age = age
stu = Student("小明", 18)
# 读取
print(stu.get_age()) # 18
# 正常修改
stu.set_age(20)
print(stu.get_age()) # 20
# 非法修改被拦截
stu.set_age(-100) # ⚠️ 年龄不合法,必须在 0-150 之间
print(stu.get_age()) # 20(没变)
这样外部只能通过 set_age() 修改,验证逻辑写在方法里,脏数据进不来。
但这种方式有个缺点——调用起来啰嗦,stu.set_age(20) 不如 stu.age = 20 直观。能不能既保护数据,又让用法像普通属性一样简洁?这就需要 property 装饰器了,我们在第四节讲。
三、命名重整机制
你可能会好奇:__age 到底藏哪去了?Python 并没有真正"隐藏"它,而是做了一层改名——这叫命名重整(Name Mangling)。
规则很简单:__属性名 会被改名为 _类名__属性名。
class Student:
def __init__(self, name, age):
self.__age = age
stu = Student("小明", 18)
# 直接访问会报错
# print(stu.__age) # AttributeError
# 但用改名后的名字居然能访问!
print(stu._Student__age) # 18
结论:Python 的私有属性不是真的"锁死",只是改了个名字,让你不太容易碰到。
这是一种"君子协定"——Python 的哲学是"我们都是成年人",不会真正阻止你访问私有属性,但通过命名约定提醒你:加了 __ 的属性是内部的,别从外面碰它。
3.1 单下划线 vs 双下划线
Python 有两种下划线约定,强度不同:
| 写法 | 名称 | 外部能否访问 | 用途 |
|---|---|---|---|
self.name |
公开属性 | ✅ 能 | 正常对外暴露的属性 |
self._name |
受保护属性(约定) | ✅ 能,但不建议 | 提示外部"这是内部的,别碰" |
self.__name |
私有属性(改名) | ❌ 不能直接访问 | 防止外部直接访问和子类覆盖 |
class Demo:
def __init__(self):
self.public = "公开"
self._protected = "受保护"
self.__private = "私有"
d = Demo()
print(d.public) # 公开
print(d._protected) # 受保护(能访问,但约定上不建议)
# print(d.__private) # ❌ AttributeError
print(d._Demo__private) # 私有(改名后能访问,但强烈不建议)
日常开发中,最常用的是单下划线
_name表示"内部使用",双下划线__name在需要避免子类命名冲突时才用。两者都靠自觉,Python 不会强制阻止你。
四、property 装饰器
上一节我们用 get_age() / set_age() 方法保护了私有属性,但调用方式不够优雅。property 装饰器能让你像访问普通属性一样调用方法,同时保持数据验证能力。
4.1 基本用法
class Student:
def __init__(self, name, age):
self.name = name
self.__age = age # 私有属性
@property
def age(self):
"""getter:读取年龄"""
return self.__age
@age.setter
def age(self, value):
"""setter:修改年龄,带验证"""
if value < 0 or value > 150:
print("⚠️ 年龄不合法,必须在 0-150 之间")
return
self.__age = value
stu = Student("小明", 18)
# 读取——像普通属性一样
print(stu.age) # 18(自动调用 @property 的 getter)
# 修改——像普通属性一样
stu.age = 20 # 自动调用 @age.setter
print(stu.age) # 20
# 非法值被拦截
stu.age = -100 # ⚠️ 年龄不合法,必须在 0-150 之间
print(stu.age) # 20(没变)
看,用法和普通属性一模一样:stu.age = 20,但背后自动走了 setter 的验证逻辑。对使用者来说毫无感知,对开发者来说数据安全有了保障。
4.2 property 三件套
| 装饰器 | 作用 | 触发时机 |
|---|---|---|
@property |
定义 getter(读取) |
obj.age 读取时触发 |
@age.setter |
定义 setter(赋值) |
obj.age = 值 赋值时触发 |
@age.deleter |
定义 deleter(删除) |
del obj.age 删除时触发 |
class Student:
def __init__(self, name, age):
self.__age = age
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value < 0 or value > 150:
raise ValueError("年龄必须在 0-150 之间")
self.__age = value
@age.deleter
def age(self):
print("年龄属性已被删除")
self.__age = None
stu = Student("小明", 18)
print(stu.age) # 18
stu.age = 20 # 走 setter
print(stu.age) # 20
del stu.age # 走 deleter:年龄属性已被删除
print(stu.age) # None
deleter用得较少,了解即可。最常用的是@property(读)和@xxx.setter(写)。
4.3 setter 中用异常替代 print
前面的例子中,验证失败时只是 print 一条提示就 return 了。更好的做法是抛出异常,强制调用方处理错误:
class Student:
def __init__(self, name, age):
self.name = name
self.__age = age
self.age = age # 初始化时也走 setter 验证!
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError("年龄必须在 0-150 之间")
self.__age = value
stu = Student("小明", 18)
print(stu.age) # 18
# 类型错误
# stu.age = "十八" # TypeError: 年龄必须是整数
# 范围错误
# stu.age = 200 # ValueError: 年龄必须在 0-150 之间
关键技巧:在
__init__中用self.age = age而非self.__age = age,这样初始化时也会走setter验证,确保对象一创建就是合法的。
五、只读属性
只定义 @property,不定义 @xxx.setter,属性就变成只读的——能读不能写:
class Circle:
def __init__(self, radius):
self.__radius = radius
@property
def radius(self):
return self.__radius
@property
def area(self):
"""面积:只读,由半径计算得出,不需要也不能手动设置"""
return 3.14159 * self.__radius ** 2
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53975
# 只读属性不能修改
# c.radius = 10 # ❌ AttributeError: can't set attribute
# 只读属性不能删除
# del c.radius # ❌ AttributeError: can't delete attribute
结论:只写
@property不写@xxx.setter= 只读属性。 适合"只能看不能改"的数据,比如圆的面积由半径算出来,不该被手动赋值。
六、数据验证实战
把前面学的串起来,看一个完整的"带验证的属性"例子:
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price # 走 setter 验证
self.stock = stock # 走 setter 验证
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if not isinstance(value, (int, float)):
raise TypeError("价格必须是数字")
if value < 0:
raise ValueError("价格不能为负数")
self.__price = value
@property
def stock(self):
return self.__stock
@stock.setter
def stock(self, value):
if not isinstance(value, int):
raise TypeError("库存必须是整数")
if value < 0:
raise ValueError("库存不能为负数")
self.__stock = value
# 正常创建
p = Product("手机", 2999, 100)
print(f"{p.name}:{p.price} 元,库存 {p.stock} 件")
# 手机:2999 元,库存 100 件
# 修改价格
p.price = 2799
print(p.price) # 2799
# 非法价格被拦截
# p.price = -10 # ValueError: 价格不能为负数
# p.stock = -5 # ValueError: 库存不能为负数
这套模式——私有属性 + property + setter 验证 + 异常抛出——就是 Python 封装的标准写法,实际开发中到处都在用。
七、Scratch 积木对照
本章涉及封装、私有属性、property 装饰器等概念,属于面向对象的高阶特性,Scratch 中没有直接对应的积木,属于无对照类型。
Scratch 的变量是全局或角色级别的,所有脚本都能直接读写,没有"私有变量"的概念,也没有"读写时自动触发验证"的机制。这些是 Python 面向对象编程独有的能力,正常学习 Python 即可。
# Scratch 没有封装概念 → Python 的封装是面向对象的高级特性
# Scratch: 所有变量都是公开的,任何积木都能读写
# Python: 可以用私有属性 + property 控制数据的读写权限
class BankAccount:
def __init__(self, balance):
self.__balance = balance # 私有属性,外部不能直接访问
@property
def balance(self):
return self.__balance # 只读,外部能看余额但不能直接改
account = BankAccount(1000)
print(account.balance) # 1000(读取正常)
# account.balance = 500 # ❌ 没有定义 setter,不能直接修改
八、综合实战:银行账户类
综合运用类定义、私有属性、property 装饰器、数据验证、异常处理,写一个银行账户管理程序:
# ========================================
# 银行账户类
# ========================================
class Account:
"""银行账户:余额私有,通过 property 控制存取款"""
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # 余额设为私有
# ---------- 余额(只读)----------
@property
def balance(self):
"""余额只读,外部不能直接赋值"""
return self.__balance
# ---------- 存款 ----------
def deposit(self, amount):
"""存款:金额必须为正数"""
if not isinstance(amount, (int, float)):
raise TypeError("存款金额必须是数字")
if amount <= 0:
raise ValueError("存款金额必须大于 0")
self.__balance += amount
print(f"✅ 存入 {amount} 元,当前余额:{self.__balance} 元")
# ---------- 取款 ----------
def withdraw(self, amount):
"""取款:金额必须为正数且不超过余额"""
if not isinstance(amount, (int, float)):
raise TypeError("取款金额必须是数字")
if amount <= 0:
raise ValueError("取款金额必须大于 0")
if amount > self.__balance:
raise ValueError(f"余额不足!当前余额仅 {self.__balance} 元,无法取出 {amount} 元")
self.__balance -= amount
print(f"✅ 取出 {amount} 元,当前余额:{self.__balance} 元")
# ---------- 转账 ----------
def transfer(self, target, amount):
"""转账:先取自己的,再存对方的"""
self.withdraw(amount) # 取款(自带验证)
target.deposit(amount) # 存入对方(自带验证)
print(f"✅ 向 {target.owner} 转账 {amount} 元成功")
# ---------- 信息展示 ----------
def __str__(self):
return f"账户[{self.owner}] 余额:{self.__balance} 元"
# ========== 测试 ==========
# 创建账户
acc1 = Account("小明", 1000)
acc2 = Account("小红", 500)
print(acc1) # 账户[小明] 余额:1000 元
print(acc2) # 账户[小红] 余额:500 元
# 余额只读,不能直接修改
# acc1.balance = 9999 # ❌ AttributeError: can't set attribute
# 存款
acc1.deposit(500)
# ✅ 存入 500 元,当前余额:1500 元
# 取款(正常)
acc1.withdraw(200)
# ✅ 取出 200 元,当前余额:1300 元
# 取款(超额——被拦截)
try:
acc1.withdraw(9999)
except ValueError as e:
print(f"❌ {e}")
# ❌ 余额不足!当前余额仅 1300 元,无法取出 9999 元
# 取款(负数——被拦截)
try:
acc1.withdraw(-100)
except ValueError as e:
print(f"❌ {e}")
# ❌ 取款金额必须大于 0
# 转账
acc1.transfer(acc2, 300)
# ✅ 取出 300 元,当前余额:1000 元
# ✅ 存入 300 元,当前余额:800 元
# ✅ 向 小红 转账 300 元成功
print(acc1) # 账户[小明] 余额:1000 元
print(acc2) # 账户[小红] 余额:800 元
运行结果:
账户[小明] 余额:1000 元
账户[小红] 余额:500 元
✅ 存入 500 元,当前余额:1500 元
✅ 取出 200 元,当前余额:1300 元
❌ 余额不足!当前余额仅 1300 元,无法取出 9999 元
❌ 取款金额必须大于 0
✅ 取出 300 元,当前余额:1000 元
✅ 存入 300 元,当前余额:800 元
✅ 向 小红 转账 1000 元成功
账户[小明] 余额:1000 元
账户[小红] 余额:800 元
分步拆解:
__balance私有属性:余额存在self.__balance中,外部无法直接读取或修改,防止绕过验证直接篡改余额@property balance(只读):外部可以通过acc1.balance查看余额,但不能赋值——只定义了getter没定义setterdeposit()存款验证:检查金额是否为数字、是否大于 0,通过后才加到余额上withdraw()取款验证:除了检查正数,还检查金额不能超过余额,超额时抛出ValueErrortransfer()转账复用:转账 = 先调自己的withdraw()+ 再调对方的deposit(),验证逻辑天然复用,不用重写try-except异常处理:调用方用try-except捕获ValueError,优雅处理错误而不让程序崩溃__str__方法:让print(acc1)输出可读信息而非默认的内存地址(__str__是魔法方法,下一章会详细讲)
九、常见报错速查表
| 报错信息 | 出现场景 | 原因 | 解决方法 |
|---|---|---|---|
AttributeError: 'xxx' object has no attribute '__age' |
访问私有属性 | 外部直接访问 obj.__age |
通过 @property 定义的 getter 访问,或用改名后的 obj._类名__age |
AttributeError: can't set attribute |
修改只读属性 | 只定义了 @property 没定义 @xxx.setter |
添加 @xxx.setter 允许写入,或通过类内方法修改 |
AttributeError: can't delete attribute |
删除只读属性 | 没定义 @xxx.deleter |
添加 @xxx.deleter 允许删除 |
ValueError: 年龄必须在 0-150 之间 |
setter 验证失败 |
赋值时触发自定义验证逻辑 | 传入合法值,或用 try-except 捕获异常 |
TypeError: 存款金额必须是数字 |
类型验证失败 | 传入非数字类型 | 确保传入 int 或 float 类型 |
RecursionError: maximum recursion depth exceeded |
setter 无限递归 |
setter 中写 self.age = value 而非 self.__age = value,反复触发自身 |
setter 中必须操作私有属性 self.__age,不能操作 property 名 |
⚠️ 最后一个递归错误是新手高频踩坑点:
setter内部一定赋值给self.__xxx,不能赋值给self.xxx,否则会无限触发setter自身。
十、本章小结
本章你掌握了 Python 面向对象中封装的核心技能:
- 为什么需要封装——公开属性可以被外部随意篡改,封装能把数据藏起来,只通过受控通道访问
- 私有属性——双下划线
__前缀让属性变为私有,外部不能直接访问;命名重整机制把它改名为_类名__属性名 - 单下划线 vs 双下划线——
_name是约定上的"内部使用",__name会触发改名,两者都靠自觉 property装饰器——@property定义getter,@xxx.setter定义setter,让方法像属性一样使用- 只读属性——只定义
@property不定义setter,属性就只能读不能写 - 数据验证——在
setter中检查值合法性,不合法就抛异常,__init__中通过self.xxx = value走setter确保初始值也合法
这套"私有属性 + property + 验证"的模式,是 Python 封装的标准范式,实际开发中无处不在。
往期回顾:
零基础学 Python 第1章 | 全平台装环境 + VS Code 配置 + 第一个 Hello World
零基础学 Python 第2章 | print 输出 + 变量赋值 + input 交互输入
零基础学 Python 第3章 | 条件判断:if / elif / else
零基础学 Python 第 4 章 | for 循环与 range():让程序帮你重复干活
零基础学 Python 第 5 章 | while 循环与循环嵌套:让程序看着条件自己干活
零基础学 Python 第 6 章 | 元组与集合:不可变数据与自动去重
零基础学 Python 第7章 | 字典 dict:键值对存储
零基础学 Python 第9章 | 函数基础:def、参数与返回值
零基础学 Python 第10章 | 函数进阶:参数灵活用法与作用域
零基础学 Python 第12章 | 文件读写:让数据持久保存
零基础学 Python 第13章 | 异常处理:try-except 让程序不崩溃
零基础学 Python 第15章 | 类与对象:面向对象编程入门
下一章预告:封装让数据安全了,但面向对象还有一大利器——多态。下一章学习多态和魔法方法(__str__、__repr__、__eq__ 等),让你的对象能用 print 优雅输出、能用 == 比较、能用 len() 获取长度,像内置类型一样好用。
文末资源区
| 资源 | 说明 |
|---|---|
| Python 官方文档 - 私有变量 | https://docs.python.org/3/tutorial/classes.html#private-variables |
Python 官方文档 - property() |
https://docs.python.org/3/library/functions.html#property |
| Python 官方文档 - 名称重整 | https://docs.python.org/3/reference/expressions.html#atom-identifiers |
| 本章完整源码 | chapter17_all.py(关注梅雅达编程笔记,评论领资料) |
每节课都配套课后练习题与完整源码。代码报错、看不懂知识点,可在评论区粘贴问题,逐一解答!
欢迎关注梅雅达编程笔记,持续分享Python零基础学习内容
更多推荐





所有评论(0)