查看全课大纲(第 33 / 51 节)
  1. 1.Python 是什么?为什么值得学?
  2. 2.如何为 Python 编程准备一个“全家桶”环境?
  3. 3.Python 有哪几种执行方法?
  4. 4.行与缩进到底有什么用?
  5. 5.注释有什么用,怎么写?
  6. 6.变量:如何给数据起个好名字?
  7. 7.运算符的优先级和常见坑点是什么?
  8. 8.Python 中,数字和布尔值究竟有哪几种?
  9. 9.输入函数 `input()` 和文件读取 `open()` 怎么用?
  10. 10.如何将程序的结果展示给用户或保存下来?
  11. 11.CSV 文件怎么读怎么写才方便?
  12. 12.列表是什么,如何创建和访问它?
  13. 13.列表的修改操作,有哪些方法?
  14. 14.列表切片为什么能取到部分元素?排序和查找又该怎么用?
  15. 15.判断语句:如何让程序根据条件做不同的事?
  16. 16.for 循环如何遍历序列中的元素?
  17. 17.while 循环什么时候才会停?
  18. 18.while 循环的结束条件,你写对了吗?
  19. 19.循环中途想提前结束,怎么办?
  20. 20.`break` 和 `continue` 到底有什么区别?
  21. 21.列表推导式如何让代码更简洁高效?
  22. 22.字符串怎么定义、拼接和操作?
  23. 23.元组:为什么定义单个元素时要加逗号?
  24. 24.集合(set)怎么用?如何快速去重和做集合运算?
  25. 25.字典是什么,为什么比列表更方便?
  26. 26.字典的增删改查与遍历,如何避免 KeyError?
  27. 27.函数里的变量,为什么有的能用有的不能用?
  28. 28.在函数内部修改全局变量,为什么有时不生效?
  29. 29.函数里能修改全局变量吗?返回值怎么用?
  30. 30.匿名函数:如何用 lambda 快速定义简单函数?
  31. 31.面向对象编程是什么,为什么需要它?
  32. 32.类中的方法和属性到底怎么定义和使用?
  33. 33.如何控制对对象内部属性和方法的访问?
  34. 34.异常发生时,程序会怎样继续执行?
  35. 35.如何组织代码:包和模块
  36. 36.`if __name__ == '__main__'` 到底有什么用?
  37. 37.如何用 IDE 的 Debug 功能排查程序错误?
  38. 38.Matplotlib 绘图的第一步该做什么?
  39. 39.Figure 是什么,如何设置图形尺寸?
  40. 40.如何自定义 Matplotlib 的坐标轴?
  41. 41.如何为图表添加图例,让不同曲线一目了然?
  42. 42.饼图怎么画才能直观展示数据占比?
  43. 43.如何用 Matplotlib 绘制散点图?
  44. 44.柱状图怎么画?
  45. 45.如何使用 Matplotlib 绘制等高线图?
  46. 46.如何在 Matplotlib 中绘制子图?
  47. 47.zip 函数如何把多个列表“打包”在一起?
  48. 48.如何给可迭代对象加上索引?
  49. 49.random 模块如何生成随机数?
  50. 50.math 模块怎么用?
  51. 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斤

这种方式虽然简单,但存在两个主要问题:

  1. 无法进行合法性校验:赋值操作 对象.属性 = 值 会直接生效,我们无法在赋值前检查这个新值是否合法。例如,体重应该是一个正数,但直接赋值无法阻止 wangcai.weight = -5 这样的非法操作。
  2. 修改逻辑分散,难以维护:如果修改属性的逻辑需要变更(例如,单位从“斤”改为“公斤”),我们需要找到代码中所有直接赋值的地方进行修改,当代码量庞大时,这将非常困难且容易出错。

使用设置器方法集中控制

为了解决上述问题,更好的做法是定义一个专门用于修改属性的方法,通常称为“设置器”(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
接头成功!
消息已经向上级汇报!
接头失败!

📝 动手练一练

  1. 练习:创建一个 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()。

小结

  1. 直接通过 对象.属性 = 值 修改属性简单但危险,无法校验且难以维护。
  2. 应通过类内部的“设置器”方法(如 set_xxx)来修改属性,以便集中加入校验和转换逻辑。
  3. 在属性或方法名前加两个下划线 __ 可将其定义为私有,私有成员只能在类的内部访问或调用。
  4. 私有属性强制外部代码必须通过我们提供的公有方法来访问数据,从而保证了数据的安全性和一致性。
  5. 私有方法用于隐藏对象的内部实现细节,只暴露必要的接口给外部。

📋 行动清单

  • 将本节 Dog 类的 weight 属性改为私有,并实现 set_weight 和 get_weight 方法,在 set_weight 中加入“体重不能超过100公斤”的校验。
  • 运行“动手练一练”中的 BankAccount 示例,并尝试添加一个 transfer(to_account, amount) 方法,实现向另一个账户转账的功能(需校验余额)。
  • 思考:如果一个类只有私有属性和对应的 getter/setter 方法,它和直接使用公有属性相比,优势在哪里?

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第4章代码包:面向对象基础(含1 个 Notebook)
    ZIP · 2KB
    下载