Python 编程:从零基础到精通
第31课:属性(@property)与访问控制
学习目标
完成本课学习后,你将能够:
- 理解为什么在面向对象编程中需要使用属性(property)来替代直接访问实例变量。
- 掌握使用
@property装饰器定义"只读"、"读写"属性的三种方法。 - 理解 Python 中通过命名约定(单下划线
_和双下划线__)实现的访问控制。 - 学会通过属性为实例变量添加验证逻辑,提升代码的健壮性和封装性。
核心概念
在面向对象编程中,我们通常不希望外部代码直接修改对象内部的数据,就像你不会让外人直接操作你家的水管总阀一样。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(半径)。
- 使用
@property将radius定义为一个只读属性。 - 添加一个
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)。 - 提供
celsius、fahrenheit(华氏度)、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 # 应引发错误
常见错误
- 忘记
@property只读属性的setter:给只读属性赋值会直接报AttributeError。这是预期的行为,说明你成功阻止了直接写入。 - 混淆装饰器顺序:必须先用
@property定义 getter,然后才能用@<name>.setter定义 setter。 - 过度使用 property:如果一个属性不需要任何验证或计算,直接使用公有变量
self.name更简单明了。不要为了"封装"而封装。 - 对名称修饰的误解:
__name并不是真正的私有,可以通过_ClassName__name访问。它的主要目的是避免子类命名冲突,而不是严格的访问控制。 - 在setter中忘记验证:属性的强大之处在于封装逻辑,如果setter只是简单赋值,那么和直接访问变量区别不大。
小结
本课我们深入探讨了 Python 面向对象编程中至关重要的封装特性:
- 核心动机:通过隐藏内部数据并控制其访问方式,来提高代码的健壮性、安全性和可维护性。
@property装饰器:它让我们能够以"属性访问"的语法(obj.attr)来调用方法,是实现"计算属性"和"带验证的属性"的利器。关键步骤是定义getter、setter(和deleter)。- 访问控制:Python 通过命名约定(
_和__)来暗示变量的用途,这是一种"君子协定"。理解名称修饰(__)的工作原理有助于避免一些隐蔽的错误。 - 设计思想:属性是"行为"的一种。一个设计良好的类,应该通过清晰的接口(方法或属性)与外界交互,而不是暴露杂乱的数据。
掌握属性,你就能写出更加专业、健壮且易于使用的 Python 类。在下一课中,我们将学习另一个重要的 OOP 协议——迭代器协议。