本章摘要

  1. 核心目标:掌握封装机制,能用私有属性和 property 装饰器保护类内数据安全
  2. 核心知识点:私有属性(__前缀)、命名重整、property 装饰器、getter/setter、只读属性、数据验证
  3. 实操产出:银行账户类,通过 property 控制存取款并验证金额合法性
  4. 前置基础:第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 不会拦你。

📷 图片占位:encapsulation-concept.png — 封装概念图(公开属性=敞开的盒子任何人可拿,私有属性=上锁的盒子只能通过指定方法访问)

封装的核心思路:把不该被随意修改的数据藏起来,只暴露安全的访问通道。外部想读数据?走 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(没变)

📷 图片占位:property-flow.png — property装饰器工作流程图(读取→@property→getter,赋值→@xxx.setter→验证→存储)

看,用法和普通属性一模一样: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 元

分步拆解:

  1. __balance 私有属性:余额存在 self.__balance 中,外部无法直接读取或修改,防止绕过验证直接篡改余额
  2. @property balance(只读):外部可以通过 acc1.balance 查看余额,但不能赋值——只定义了 getter 没定义 setter
  3. deposit() 存款验证:检查金额是否为数字、是否大于 0,通过后才加到余额上
  4. withdraw() 取款验证:除了检查正数,还检查金额不能超过余额,超额时抛出 ValueError
  5. transfer() 转账复用:转账 = 先调自己的 withdraw() + 再调对方的 deposit(),验证逻辑天然复用,不用重写
  6. try-except 异常处理:调用方用 try-except 捕获 ValueError,优雅处理错误而不让程序崩溃
  7. __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: 存款金额必须是数字 类型验证失败 传入非数字类型 确保传入 intfloat 类型
RecursionError: maximum recursion depth exceeded setter 无限递归 setter 中写 self.age = value 而非 self.__age = value,反复触发自身 setter 中必须操作私有属性 self.__age,不能操作 property

⚠️ 最后一个递归错误是新手高频踩坑点:setter 内部一定赋值给 self.__xxx,不能赋值给 self.xxx,否则会无限触发 setter 自身。


十、本章小结

本章你掌握了 Python 面向对象中封装的核心技能:

  1. 为什么需要封装——公开属性可以被外部随意篡改,封装能把数据藏起来,只通过受控通道访问
  2. 私有属性——双下划线 __ 前缀让属性变为私有,外部不能直接访问;命名重整机制把它改名为 _类名__属性名
  3. 单下划线 vs 双下划线——_name 是约定上的"内部使用",__name 会触发改名,两者都靠自觉
  4. property 装饰器——@property 定义 getter@xxx.setter 定义 setter,让方法像属性一样使用
  5. 只读属性——只定义 @property 不定义 setter,属性就只能读不能写
  6. 数据验证——在 setter 中检查值合法性,不合法就抛异常,__init__ 中通过 self.xxx = valuesetter 确保初始值也合法

这套"私有属性 + property + 验证"的模式,是 Python 封装的标准范式,实际开发中无处不在。

往期回顾:

《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 第8章 | 字符串操作与格式化输出

零基础学 Python 第9章 | 函数基础:def、参数与返回值

零基础学 Python 第10章 | 函数进阶:参数灵活用法与作用域

零基础学 Python 第11章 | 匿名函数与高阶函数

零基础学 Python 第12章 | 文件读写:让数据持久保存

零基础学 Python 第13章 | 异常处理:try-except 让程序不崩溃

零基础学 Python 第14章 | 模块、包与第三方库

零基础学 Python 第15章 | 类与对象:面向对象编程入门

零基础学 Python 第16章 | 继承与方法重写

下一章预告:封装让数据安全了,但面向对象还有一大利器——多态。下一章学习多态和魔法方法(__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零基础学习内容

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐