31·模块七:面向对象编程进阶

属性(@property)与访问控制

ooppropertyencapsulation

Python 编程:从零基础到精通

第31课:属性(@property)与访问控制

学习目标

完成本课学习后,你将能够:

  1. 理解为什么在面向对象编程中需要使用属性(property)来替代直接访问实例变量。
  2. 掌握使用 @property 装饰器定义"只读"、"读写"属性的三种方法。
  3. 理解 Python 中通过命名约定(单下划线 _ 和双下划线 __)实现的访问控制。
  4. 学会通过属性为实例变量添加验证逻辑,提升代码的健壮性和封装性。

核心概念

在面向对象编程中,我们通常不希望外部代码直接修改对象内部的数据,就像你不会让外人直接操作你家的水管总阀一样。Python 通过"属性"(property)和访问控制来实现这种"封装"。

1. 为什么不直接用 self.variable 直接暴露实例变量(如 self.age = age)有两个主要问题:

  • 无法验证:用户可以传入任何值,比如 person.age = -5,这显然不合理。
  • 修改困难:如果我们将来想改变数据的存储方式(例如,将年龄改为出生年份),所有直接访问 self.age 的代码都需要修改。

2. 解决方案:方法与属性 我们可以将变量设置为"私有",并提供公共的"方法"(如 get_age()set_age()) 来访问和修改它。这就像给水管装上净水器和调压阀。

class Person:
    def __init__(self, age):
        self._age = age  # 约定:单下划线表示这是"受保护"的内部变量

    def get_age(self):
        return self._age

    def set_age(self, value):
        if not isinstance(value, int) or value < 0 or value > 150:
            raise ValueError("年龄必须是0到150之间的整数")
        self._age = value

虽然解决了问题,但写法从 p.age 变成了 p.get_age(),不够优雅。

3. @property 装饰器:让方法像属性一样被访问 @property 可以将一个方法"伪装"成一个属性。访问它时不需要加括号。

  • @property: 定义一个 getter 方法。
  • @<name>.setter: 定义一个 setter 方法。
  • @<name>.deleter: 定义一个 deleter 方法(不常用)。

这样,我们既保留了 p.age 这样简洁的访问语法,又在内部执行了验证逻辑。

4. Python 的访问控制("君子协定") Python 没有像 private 这样的强制关键字,它通过命名约定来提示程序员:

  • name: 公有变量,任何地方都能访问。
  • _name: 单下划线开头。这是一个"约定",表示这是一个内部使用的变量,不应在类外部直接访问。Python 解释器不会阻止你,但这是一种强烈的提示。
  • __name: 双下划线开头。这会触发名称修饰(Name Mangling),解释器会将 _ClassName__name 作为变量名。这主要用于避免与子类中的同名变量冲突,并提供更强的"私有"暗示。

代码示例

下面是一个完整的示例,演示了从直接访问到使用 @property 的演变,并加入了验证逻辑。

class BankAccount:
    """银行账户类,演示@property和访问控制"""

    def __init__(self, owner, balance=0):
        self.owner = owner  # 公有属性
        self.__balance = balance  # 双下划线,强私有(名称修饰)

    # 使用 @property 定义只读属性
    @property
    def balance(self):
        """获取余额(只读)"""
        return self.__balance

    # 定义一个受保护的内部方法
    def _validate_amount(self, amount):
        """验证金额是否为正数"""
        if not isinstance(amount, (int, float)):
            raise TypeError("金额必须是数字")
        if amount <= 0:
            raise ValueError("金额必须大于0")

    # 存款方法(也可以封装成属性,但这里用方法更清晰)
    def deposit(self, amount):
        self._validate_amount(amount)
        self.__balance += amount
        print(f"存入 {amount} 元,当前余额: {self.__balance} 元")

    # 使用 @property 和 @setter 定义一个读写属性
    @property
    def overdraft_limit(self):
        """透支额度(可读写)"""
        return self.__overdraft_limit

    @overdraft_limit.setter
    def overdraft_limit(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("额度必须是数字")
        if value < 0:
            raise ValueError("额度不能为负数")
        self.__overdraft_limit = value
        print(f"透支额度已设置为 {value} 元")

# 创建账户并测试
account = BankAccount("张三", 1000)
account.deposit(500)  # 存入 500 元,当前余额: 1500 元

# 测试只读属性
print(f"账户余额: {account.balance} 元")  # 访问属性,不加括号
# account.balance = 2000  # 这行会报错: AttributeError: can't set attribute

# 测试读写属性
account.overdraft_limit = 2000  # 设置透支额度,触发setter验证
print(f"当前透支额度: {account.overdraft_limit} 元")

# 测试访问控制
print(f"账户持有人: {account.owner}")  # 公有属性,正常访问
# print(account.__balance)  # 会报错: AttributeError: 'BankAccount' object has no attribute '__balance'
print(f"内部余额(名称修饰后): {account._BankAccount__balance}")  # 通过修饰后的名称强制访问(不推荐)

# 测试验证
try:
    account.deposit(-100)
except ValueError as e:
    print(f"存款失败: {e}")  # 存款失败: 金额必须大于0

try:
    account.overdraft_limit = -500
except ValueError as e:
    print(f"设置额度失败: {e}")  # 设置额度失败: 额度不能为负数

实践练习

练习 1:基本属性使用 创建一个 Circle 类,它有一个私有属性 __radius(半径)。

  • 使用 @propertyradius 定义为一个只读属性。
  • 添加一个 area 的只读属性,返回圆的面积(πr²)。
# 预期用法
c = Circle(5)
print(c.radius)  # 输出: 5
print(c.area)    # 输出: 78.53981633974483

练习 2:带验证的属性 在练习 1 的基础上,修改 radius 属性,使其可以通过 setter 修改,但要添加验证:半径必须是一个正数。

# 预期用法
c = Circle(5)
c.radius = 10    # 应该成功
print(c.area)    # 输出: 314.1592653589793
c.radius = -2    # 应该引发 ValueError: "半径必须大于0"

练习 3:封装实践 设计一个 Temperature 类,用于表示温度。

  • 内部存储使用摄氏度(__celsius)。
  • 提供 celsiusfahrenheit(华氏度)、kelvin(开尔文)三个读写属性。它们应该相互关联,修改其中一个,其他的值也应该随之改变。
  • 添加验证:绝对零度以下的温度是无效的(开尔文温度不能小于0)。
# 预期用法(仅示意逻辑关系)
t = Temperature(25)
print(t.celsius)     # 25
print(t.fahrenheit)  # 77.0
t.fahrenheit = 100
print(t.celsius)     # 应约为 37.777...
t.kelvin = -10       # 应引发错误

常见错误

  1. 忘记 @property 只读属性的setter:给只读属性赋值会直接报 AttributeError。这是预期的行为,说明你成功阻止了直接写入。
  2. 混淆装饰器顺序:必须先用 @property 定义 getter,然后才能用 @<name>.setter 定义 setter。
  3. 过度使用 property:如果一个属性不需要任何验证或计算,直接使用公有变量 self.name 更简单明了。不要为了"封装"而封装。
  4. 对名称修饰的误解__name 并不是真正的私有,可以通过 _ClassName__name 访问。它的主要目的是避免子类命名冲突,而不是严格的访问控制。
  5. 在setter中忘记验证:属性的强大之处在于封装逻辑,如果setter只是简单赋值,那么和直接访问变量区别不大。

小结

本课我们深入探讨了 Python 面向对象编程中至关重要的封装特性:

  • 核心动机:通过隐藏内部数据并控制其访问方式,来提高代码的健壮性、安全性和可维护性。
  • @property 装饰器:它让我们能够以"属性访问"的语法(obj.attr)来调用方法,是实现"计算属性"和"带验证的属性"的利器。关键步骤是定义 gettersetter(和 deleter)。
  • 访问控制:Python 通过命名约定(___)来暗示变量的用途,这是一种"君子协定"。理解名称修饰(__)的工作原理有助于避免一些隐蔽的错误。
  • 设计思想:属性是"行为"的一种。一个设计良好的类,应该通过清晰的接口(方法或属性)与外界交互,而不是暴露杂乱的数据。

掌握属性,你就能写出更加专业、健壮且易于使用的 Python 类。在下一课中,我们将学习另一个重要的 OOP 协议——迭代器协议。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「迭代器协议(__iter__, __next__)」 以巩固所学知识。