查看全课大纲(第 33 / 51 节)
- 1.Python 是什么?为什么值得学?
- 2.如何为 Python 编程准备一个“全家桶”环境?
- 3.Python 有哪几种执行方法?
- 4.行与缩进到底有什么用?
- 5.注释有什么用,怎么写?
- 6.变量:如何给数据起个好名字?
- 7.运算符的优先级和常见坑点是什么?
- 8.Python 中,数字和布尔值究竟有哪几种?
- 9.输入函数 `input()` 和文件读取 `open()` 怎么用?
- 10.如何将程序的结果展示给用户或保存下来?
- 11.CSV 文件怎么读怎么写才方便?
- 12.列表是什么,如何创建和访问它?
- 13.列表的修改操作,有哪些方法?
- 14.列表切片为什么能取到部分元素?排序和查找又该怎么用?
- 15.判断语句:如何让程序根据条件做不同的事?
- 16.for 循环如何遍历序列中的元素?
- 17.while 循环什么时候才会停?
- 18.while 循环的结束条件,你写对了吗?
- 19.循环中途想提前结束,怎么办?
- 20.`break` 和 `continue` 到底有什么区别?
- 21.列表推导式如何让代码更简洁高效?
- 22.字符串怎么定义、拼接和操作?
- 23.元组:为什么定义单个元素时要加逗号?
- 24.集合(set)怎么用?如何快速去重和做集合运算?
- 25.字典是什么,为什么比列表更方便?
- 26.字典的增删改查与遍历,如何避免 KeyError?
- 27.函数里的变量,为什么有的能用有的不能用?
- 28.在函数内部修改全局变量,为什么有时不生效?
- 29.函数里能修改全局变量吗?返回值怎么用?
- 30.匿名函数:如何用 lambda 快速定义简单函数?
- 31.面向对象编程是什么,为什么需要它?
- 32.类中的方法和属性到底怎么定义和使用?
- 33.如何控制对对象内部属性和方法的访问?
- 34.异常发生时,程序会怎样继续执行?
- 35.如何组织代码:包和模块
- 36.`if __name__ == '__main__'` 到底有什么用?
- 37.如何用 IDE 的 Debug 功能排查程序错误?
- 38.Matplotlib 绘图的第一步该做什么?
- 39.Figure 是什么,如何设置图形尺寸?
- 40.如何自定义 Matplotlib 的坐标轴?
- 41.如何为图表添加图例,让不同曲线一目了然?
- 42.饼图怎么画才能直观展示数据占比?
- 43.如何用 Matplotlib 绘制散点图?
- 44.柱状图怎么画?
- 45.如何使用 Matplotlib 绘制等高线图?
- 46.如何在 Matplotlib 中绘制子图?
- 47.zip 函数如何把多个列表“打包”在一起?
- 48.如何给可迭代对象加上索引?
- 49.random 模块如何生成随机数?
- 50.math 模块怎么用?
- 51.Counter 如何快速统计元素出现次数?
如何控制对对象内部属性和方法的访问?
约 11 分钟
小象实战讲义 · Python 编程从入门到实践
在学习了如何定义类和创建对象之后,我们面临一个新的问题:如何安全地修改对象的属性?直接修改虽然简单,但可能导致数据不合法或难以维护。本节将介绍通过“设置器”方法(Setter)和“私有属性/方法”来控制访问权限,这是构建健壮、可维护的面向对象程序的关键一步。本节内容承接上一节对类、对象、属性和方法的基础介绍,并为后续学习更复杂的类设计打下基础。
💡 核心导读
- 直接修改属性的隐患:直接赋值无法进行合法性校验,且修改逻辑分散,难以统一维护。
- 使用设置器方法:通过类内部的方法来修改属性,可以在方法内加入校验和转换逻辑,集中控制修改行为。
- 定义私有属性:在属性名前加两个下划线
__,使其只能在类内部访问,外部直接访问会报错。 - 定义私有方法:同样在方法名前加两个下划线
__,使其只能在类内部调用,用于封装不希望外部直接调用的内部逻辑。
直接修改属性的问题
在 Python 中,修改一个对象属性的最直接方式是使用点号 . 进行赋值。
class Dog:
def __init__(self, name, weight):
self.name = name
self.weight = weight # 假设单位是"斤"
# 创建对象
wangcai = Dog('旺财', 10)
laifu = Dog('来福', 18)
atu = Dog('阿土', 20)
# 直接修改属性
wangcai.weight = 5
laifu.weight = 9
atu.weight = 12
print("旺财的体重:{}斤".format(wangcai.weight))
print("来福的体重:{}斤".format(laifu.weight))
print("阿土的体重:{}斤".format(atu.weight))旺财的体重:5斤
来福的体重:9斤
阿土的体重:12斤这种方式虽然简单,但存在两个主要问题:
- 无法进行合法性校验:赋值操作
对象.属性 = 值会直接生效,我们无法在赋值前检查这个新值是否合法。例如,体重应该是一个正数,但直接赋值无法阻止wangcai.weight = -5这样的非法操作。 - 修改逻辑分散,难以维护:如果修改属性的逻辑需要变更(例如,单位从“斤”改为“公斤”),我们需要找到代码中所有直接赋值的地方进行修改,当代码量庞大时,这将非常困难且容易出错。
使用设置器方法集中控制
为了解决上述问题,更好的做法是定义一个专门用于修改属性的方法,通常称为“设置器”(Setter)。所有对属性的修改都通过调用这个方法来完成。
class Dog:
def __init__(self, name, weight):
self.name = name
self.weight = weight # 内部存储单位仍为"斤"
def set_weight(self, new_weight):
# 可以在这里加入校验逻辑
if new_weight <= 0:
print("错误:体重必须为正数!")
return
# 也可以在这里加入转换逻辑(例如单位转换)
self.weight = new_weight
def get_weight(self):
# 提供一个获取体重的方法,可以统一输出格式或单位
return self.weight
# 创建对象
wangcai = Dog('旺财', 10)
# 通过设置器方法修改属性
wangcai.set_weight(5)
print("旺财的体重:{}斤".format(wangcai.get_weight()))
# 尝试设置非法值
wangcai.set_weight(-3)
print("旺财的体重:{}斤".format(wangcai.get_weight())) # 体重未改变
class Dog_Kg:
def __init__(self, name, weight_kg):
self.name = name
self.__weight_kg = weight_kg # 内部存储单位改为"公斤"
def set_weight(self, new_weight_jin):
# 外部仍以"斤"为单位传入,内部自动转换为"公斤"存储
if new_weight_jin <= 0:
print("错误:体重必须为正数!")
return
self.__weight_kg = new_weight_jin / 2 # 斤转公斤
print("已将 {}斤 转换为 {}公斤 并存储。".format(new_weight_jin, self.__weight_kg))
def get_weight(self):
# 返回公斤数
return self.__weight_kg
wangcai_kg = Dog_Kg('旺财', 5) # 初始5公斤
wangcai_kg.set_weight(10) # 传入10斤
print("旺财的体重:{}公斤".format(wangcai_kg.get_weight()))旺财的体重:5斤
错误:体重必须为正数!
旺财的体重:5斤
已将 10斤 转换为 5.0公斤 并存储。
旺财的体重:5.0公斤优势:
- 集中校验:所有对
weight的修改都必须经过set_weight方法,可以方便地加入合法性检查。 - 易于维护:当需求变更时(例如单位改为公斤),只需修改
set_weight和get_weight方法内部的逻辑即可,外部调用代码无需改动。
定义私有属性
尽管我们提供了 set_weight 方法,但用户仍然可以直接通过 对象.weight = 值 来绕过我们的校验逻辑。为了彻底禁止这种直接访问,Python 提供了私有属性。
在属性名前加上两个下划线 __,该属性就会变成私有属性,只能在类的内部访问,在类的外部直接访问会引发 AttributeError。
class Dog_Private:
def __init__(self, name, age):
self.name = name # 公有属性
self.__age = age # 私有属性,前面有两个下划线
def get_age(self):
# 类内部可以访问私有属性
return self.__age
def set_age(self, new_age):
if new_age < 0 or new_age > 30:
print("错误:年龄不合法!")
return
self.__age = new_age
wangcai_private = Dog_Private('旺财', 2)
# 访问公有属性 - 成功
print("姓名(公有属性):{}".format(wangcai_private.name))
# 访问私有属性 - 失败
# print(wangcai_private.__age) # 取消注释会报错:AttributeError
# 通过公有方法访问私有属性 - 成功
print("年龄(通过方法访问):{}岁".format(wangcai_private.get_age()))
# 通过公有方法修改私有属性 - 成功
wangcai_private.set_age(3)
print("修改后年龄:{}岁".format(wangcai_private.get_age()))
# 尝试通过公有方法设置非法年龄
wangcai_private.set_age(50)
print("尝试修改后年龄:{}岁".format(wangcai_private.get_age())) # 年龄未变姓名(公有属性):旺财
年龄(通过方法访问):2岁
修改后年龄:3岁
错误:年龄不合法!
尝试修改后年龄:3岁定义私有方法
与私有属性类似,我们也可以定义私有方法。在方法名前加两个下划线 __,该方法就成为私有方法,只能在类的内部被调用。
私有方法常用于封装那些属于对象内部实现细节、不希望被外部直接调用的逻辑。
class Comrade:
def answer_secret(self, secret):
"""对外公开的接口,用于验证暗号"""
if secret == "芝麻开门":
print("接头成功!")
self.__send_message() # 内部调用私有方法
else:
print("接头失败!")
def __send_message(self):
"""私有方法,只能在类内部调用。用于向上级发送消息。"""
print("消息已经向上级汇报!")
# 创建同志对象
comrade = Comrade()
# 调用公开方法,传入正确暗号
comrade.answer_secret("芝麻开门")
# 调用公开方法,传入错误暗号
comrade.answer_secret("天王盖地虎")
# 尝试直接调用私有方法 - 失败
# comrade.__send_message() # 取消注释会报错:AttributeError接头成功!
消息已经向上级汇报!
接头失败!📝 动手练一练
- 练习:创建一个
BankAccount类,具有私有属性__balance(余额)。要求:- 提供
deposit(amount)方法存款(金额需为正数)。 - 提供
withdraw(amount)方法取款(金额需为正数且不能超过余额)。 - 提供
get_balance()方法查询余额。 - 在类的外部直接访问
__balance应该失败。
- 提供
👉 点击查看参考答案
思路:将余额设为私有属性,所有操作通过公有方法进行,并在方法内加入合法性校验。
class BankAccount:
def __init__(self, initial_balance=0):
if initial_balance < 0:
print("初始余额不能为负,已设置为0。")
self.__balance = 0
else:
self.__balance = initial_balance
def deposit(self, amount):
if amount <= 0:
print("存款金额必须大于0。")
return False
self.__balance += amount
print("成功存入 {} 元。".format(amount))
return True
def withdraw(self, amount):
if amount <= 0:
print("取款金额必须大于0。")
return False
if amount > self.__balance:
print("余额不足。")
return False
self.__balance -= amount
print("成功取出 {} 元。".format(amount))
return True
def get_balance(self):
return self.__balance
# 测试代码
account = BankAccount(100)
print("初始余额:{}元".format(account.get_balance()))
account.deposit(50)
print("存款后余额:{}元".format(account.get_balance()))
account.withdraw(30)
print("取款后余额:{}元".format(account.get_balance()))
account.withdraw(200) # 尝试超额取款
print("尝试超额取款后余额:{}元".format(account.get_balance()))
# print(account.__balance) # 这行会报错初始余额:100元
成功存入 50 元。
存款后余额:150元
成功取出 30 元。
取款后余额:120元
余额不足。
尝试超额取款后余额:120元常见错误
| 错误写法 | 现象或报错 | 原因 | 正确写法 |
|---|---|---|---|
print(dog.__age) | AttributeError: 'Dog' object has no attribute '__age' | 在类外部直接访问了私有属性,外部无法直接按原名访问。 | 通过类提供的公有方法访问,如 dog.get_age()。 |
self.__age = -5 (在类内部方法中) | 无报错,但数据不合法。 | 私有属性虽然限制了外部访问,但类内部的方法仍然可以直接修改它。需要在修改方法中加入合法性校验。 | 在设置属性的方法中加入判断:if new_age >= 0: self.__age = new_age。 |
def my_private_method(self): (想作为私有方法) | 外部仍可调用 obj.my_private_method()。 | 方法名前没有加两个下划线 __,Python不会将其视为私有方法。 | 使用双下划线前缀:def __my_private_method(self):。 |
obj.__private_method() | AttributeError | 在类外部尝试直接调用私有方法。 | 私有方法只能在类内部的其他方法中调用,如 self.__private_method()。 |
小结
- 直接通过
对象.属性 = 值修改属性简单但危险,无法校验且难以维护。 - 应通过类内部的“设置器”方法(如
set_xxx)来修改属性,以便集中加入校验和转换逻辑。 - 在属性或方法名前加两个下划线
__可将其定义为私有,私有成员只能在类的内部访问或调用。 - 私有属性强制外部代码必须通过我们提供的公有方法来访问数据,从而保证了数据的安全性和一致性。
- 私有方法用于隐藏对象的内部实现细节,只暴露必要的接口给外部。
📋 行动清单
- 将本节
Dog类的weight属性改为私有,并实现set_weight和get_weight方法,在set_weight中加入“体重不能超过100公斤”的校验。 - 运行“动手练一练”中的
BankAccount示例,并尝试添加一个transfer(to_account, amount)方法,实现向另一个账户转账的功能(需校验余额)。 - 思考:如果一个类只有私有属性和对应的
getter/setter方法,它和直接使用公有属性相比,优势在哪里?
—— 小象教研组
配套资料 · 直接下载
课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。
- 下载第4章代码包:面向对象基础(含1 个 Notebook)ZIP · 2KB