第62集:属性装饰器
学习目标
- 理解@property装饰器的作用和优势
- 掌握@property、@setter、@deleter的完整用法
- 学会使用属性装饰器实现属性的控制和验证
- 了解属性装饰器与传统getter/setter方法的区别
1. 为什么需要属性装饰器
1.1 传统属性访问的问题
在没有属性装饰器之前,我们通常这样控制属性访问:
class Person:
def __init__(self, name, age):
self._name = name # 使用下划线表示"私有"属性
self._age = age
# Getter方法
def get_age(self):
return self._age
# Setter方法
def set_age(self, value):
if value < 0 or value > 150:
raise ValueError("年龄必须在0-150之间")
self._age = value
# 使用起来很麻烦
person = Person("张三", 25)
person.set_age(30) # 而不是直接 person.age = 30
current_age = person.get_age() # 而不是直接 person.age1.2 属性装饰器的解决方案
属性装饰器让我们可以用访问属性的方式来调用方法:
class Person:
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:
raise ValueError("年龄必须在0-150之间")
self._age = value
# 使用起来就像普通属性一样
person = Person("张三", 25)
person.age = 30 # 调用setter方法
print(person.age) # 调用getter方法,输出:302. @property装饰器基础
2.1 基本语法
class ClassName:
@property
def property_name(self):
# getter逻辑
return self._attribute_name2.2 简单示例
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""圆的半径"""
return self._radius
@property
def diameter(self):
"""圆的直径(计算属性)"""
return self._radius * 2
@property
def area(self):
"""圆的面积(计算属性)"""
return 3.14159 * self._radius ** 2
@property
def circumference(self):
"""圆的周长(计算属性)"""
return 2 * 3.14159 * self._radius
# 使用示例
circle = Circle(5)
print(f"半径:{circle.radius}") # 5
print(f"直径:{circle.diameter}") # 10
print(f"面积:{circle.area:.2f}") # 78.54
print(f"周长:{circle.circumference:.2f}") # 31.42
# 注意:这些是只读属性,不能赋值
# circle.radius = 10 # AttributeError: can't set attribute3. @setter装饰器
3.1 基本语法
class ClassName:
@property
def property_name(self):
return self._attribute_name
@property_name.setter
def property_name(self, value):
# setter逻辑和验证
self._attribute_name = value3.2 带验证的属性
class Temperature:
"""温度类 - 演示setter的验证逻辑"""
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""摄氏温度"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""设置摄氏温度,带范围验证"""
if not isinstance(value, (int, float)):
raise TypeError("温度必须是数字")
if value < -273.15: # 绝对零度
raise ValueError("温度不能低于绝对零度(-273.15°C)")
self._celsius = value
@property
def fahrenheit(self):
"""华氏温度(只读)"""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""通过华氏温度设置摄氏温度"""
if not isinstance(value, (int, float)):
raise TypeError("温度必须是数字")
# 转换为摄氏温度
celsius = (value - 32) * 5/9
if celsius < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = celsius
# 使用示例
temp = Temperature(25)
print(f"摄氏温度:{temp.celsius}°C") # 25°C
print(f"华氏温度:{temp.fahrenheit:.1f}°F") # 77.0°F
# 通过华氏温度设置
temp.fahrenheit = 100
print(f"设置华氏100°F后,摄氏温度:{temp.celsius:.1f}°C") # 37.8°C
# 验证边界值
try:
temp.celsius = -300 # 低于绝对零度
except ValueError as e:
print(f"错误:{e}") # 温度不能低于绝对零度(-273.15°C)4. @deleter装饰器
4.1 基本语法
class ClassName:
@property
def property_name(self):
return self._attribute_name
@property_name.setter
def property_name(self, value):
self._attribute_name = value
@property_name.deleter
def property_name(self):
# 删除时的清理逻辑
del self._attribute_name4.2 删除操作的示例
class BankAccount:
"""银行账户类 - 演示deleter的使用"""
def __init__(self, account_holder, balance=0):
self.account_holder = account_holder
self._balance = balance
self._is_active = True
@property
def balance(self):
"""账户余额"""
if not self._is_active:
raise ValueError("账户已被关闭")
return self._balance
@balance.setter
def balance(self, value):
if not self._is_active:
raise ValueError("账户已被关闭,无法修改余额")
if value < 0:
raise ValueError("余额不能为负数")
self._balance = value
@balance.deleter
def balance(self):
"""删除余额属性(关闭账户)"""
print(f"正在关闭账户:{self.account_holder}")
self._balance = 0
self._is_active = False
print("账户已关闭,余额清零")
def close_account(self):
"""显式关闭账户的方法"""
del self.balance
# 使用示例
account = BankAccount("张三", 1000)
print(f"初始余额:{account.balance}元") # 1000元
account.balance = 1500
print(f"存款后余额:{account.balance}元") # 1500元
# 关闭账户
del account.balance
# 尝试访问已关闭账户的余额
try:
print(account.balance)
except ValueError as e:
print(f"错误:{e}") # 账户已被关闭5. 完整的三重装饰器示例
让我们创建一个完整的用户类来展示@property、@setter、@deleter的完整配合:
class User:
"""用户类 - 完整展示三重装饰器"""
def __init__(self, username, email, age=0):
self.username = username
self._email = email
self._age = age
self._password = None
self._login_attempts = 0
# 用户名属性
@property
def username(self):
return self._username
@username.setter
def username(self, value):
if not isinstance(value, str) or len(value.strip()) == 0:
raise ValueError("用户名不能为空")
if len(value) < 3 or len(value) > 20:
raise ValueError("用户名长度必须在3-20个字符之间")
self._username = value.strip()
# 邮箱属性
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if not isinstance(value, str):
raise TypeError("邮箱必须是字符串")
# 简单的邮箱格式验证
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, value):
raise ValueError("邮箱格式不正确")
self._email = value
# 年龄属性
@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
# 密码属性(私有,有特殊逻辑)
@property
def password(self):
raise AttributeError("密码不可读取,为了安全考虑")
@password.setter
def password(self, value):
if not isinstance(value, str):
raise TypeError("密码必须是字符串")
if len(value) < 6:
raise ValueError("密码长度至少6位")
# 实际应用中应该加密存储
import hashlib
self._password = hashlib.md5(value.encode()).hexdigest()
print("密码设置成功")
@password.deleter
def password(self):
self._password = None
print("密码已清除")
# 登录尝试次数(内部使用)
@property
def login_attempts(self):
return self._login_attempts
@login_attempts.setter
def login_attempts(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError("登录尝试次数不能为负数")
self._login_attempts = value
@login_attempts.deleter
def login_attempts(self):
self._login_attempts = 0
print("登录尝试次数已重置")
# 计算属性:用户等级
@property
def level(self):
"""根据用户年龄返回等级"""
if self._age < 13:
return "儿童"
elif self._age < 18:
return "青少年"
elif self._age < 60:
return "成人"
else:
return "老年"
# 计算属性:用户信息摘要
@property
def info(self):
"""返回用户信息摘要(只读)"""
return {
'username': self._username,
'email': self._email,
'age': self._age,
'level': self.level,
'has_password': self._password is not None
}
def verify_password(self, password):
"""验证密码"""
if self._password is None:
return False
import hashlib
return self._password == hashlib.md5(password.encode()).hexdigest()使用示例:
# 创建用户
user = User("john_doe", "john@example.com", 25)
print(f"用户名:{user.username}")
print(f"邮箱:{user.email}")
print(f"年龄:{user.age}")
print(f"等级:{user.level}")
# 设置密码
user.password = "mypassword123"
print(f"密码设置:{'成功' if user.verify_password('mypassword123') else '失败'}")
# 获取用户信息
print("用户信息:", user.info)
# 尝试读取密码(会报错)
try:
print(user.password)
except AttributeError as e:
print(f"安全保护:{e}")
# 重置登录尝试次数
del user.login_attempts
print(f"登录尝试次数:{user.login_attempts}")6. 实际应用场景
6.1 数据验证和清理
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 = round(float(value), 2)
@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
@property
def total_value(self):
"""库存总价值(计算属性)"""
return self._price * self._stock
# 使用示例
product = Product("笔记本电脑", 4999.999, 10)
print(f"价格:{product.price}") # 5000.0(自动四舍五入到两位小数)
print(f"库存:{product.stock}") # 10
print(f"库存总价值:{product.total_value}") # 50000.06.2 延迟计算(惰性求值)
class DataAnalyzer:
"""数据分析器 - 演示延迟计算"""
def __init__(self, data):
self._data = data
self._cached_stats = None
self._stats_dirty = True
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
self._stats_dirty = True # 数据改变,标记统计信息为过期
@property
def statistics(self):
"""统计数据(延迟计算,只有第一次访问时计算)"""
if self._stats_dirty or self._cached_stats is None:
print("正在计算统计数据...")
# 模拟复杂的统计计算
self._cached_stats = {
'count': len(self._data),
'sum': sum(self._data),
'average': sum(self._data) / len(self._data) if self._data else 0,
'max': max(self._data) if self._data else None,
'min': min(self._data) if self._data else None
}
self._stats_dirty = False
return self._cached_stats
@statistics.deleter
def statistics(self):
"""清除缓存的统计信息"""
self._cached_stats = None
self._stats_dirty = True
print("统计信息缓存已清除")
# 使用示例
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
analyzer = DataAnalyzer(data)
print("第一次访问统计信息:")
stats1 = analyzer.statistics
print(stats1)
print("\n第二次访问统计信息(使用缓存):")
stats2 = analyzer.statistics
print(stats2)
# 修改数据
analyzer.data = [10, 20, 30, 40, 50]
print("\n修改数据后访问统计信息:")
stats3 = analyzer.statistics
print(stats3)6.3 访问控制和安全
class SecureDocument:
"""安全文档类 - 演示访问控制"""
def __init__(self, title, content, access_level="public"):
self.title = title
self._content = content
self.access_level = access_level
self._view_count = 0
@property
def access_level(self):
return self._access_level
@access_level.setter
def access_level(self, value):
valid_levels = ["public", "internal", "confidential", "secret"]
if value not in valid_levels:
raise ValueError(f"访问级别必须是:{', '.join(valid_levels)}")
self._access_level = value
@property
def content(self):
"""文档内容 - 根据访问级别控制显示"""
self._view_count += 1
if self._access_level == "public":
return self._content
elif self._access_level == "internal":
return self._content[:50] + "..." if len(self._content) > 50 else self._content
else:
return f"[受保护内容 - {self._access_level}级别]"
@content.setter
def content(self, value):
# 只有secret级别才能修改内容
if self._access_level != "secret":
raise PermissionError("只有secret级别可以修改文档内容")
self._content = value
@property
def view_count(self):
"""查看次数(只读)"""
return self._view_count
@property
def can_edit(self):
"""是否可以编辑(计算属性)"""
return self._access_level == "secret"
# 使用示例
doc = SecureDocument("项目计划", "这是一个非常重要的项目计划文档...", "internal")
print(f"访问级别:{doc.access_level}")
print(f"文档内容:{doc.content}")
print(f"查看次数:{doc.view_count}")
print(f"可以编辑:{doc.can_edit}")
# 尝试修改内容(会失败)
try:
doc.content = "新的内容"
except PermissionError as e:
print(f"权限错误:{e}")7. 属性装饰器 vs 传统getter/setter
7.1 语法对比
# 传统方式
class TraditionalPerson:
def __init__(self, age):
self._age = age
def get_age(self):
return self._age
def set_age(self, value):
self._age = value
# 使用
person1 = TraditionalPerson(25)
person1.set_age(30)
age = person1.get_age()
# 属性装饰器方式
class ModernPerson:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
self._age = value
# 使用
person2 = ModernPerson(25)
person2.age = 30 # 像属性一样使用
age = person2.age7.2 优势对比
| 特性 | 传统getter/setter | 属性装饰器 |
|---|---|---|
| 语法简洁性 | ❌ 繁琐 | ✅ 简洁 |
| 向后兼容 | ✅ 好 | ⚠️ 可能破坏现有代码 |
| 性能 | ✅ 稍快 | ⚠️ 稍慢(方法调用开销) |
| IDE支持 | ⚠️ 一般 | ✅ 好 |
| 可读性 | ❌ 差 | ✅ 好 |
8. 常见错误与注意事项
8.1 忘记设置setter导致只读属性
class ReadOnlyExample:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
# 忘记写setter,变成只读属性
obj = ReadOnlyExample(10)
print(obj.value) # 10
# obj.value = 20 # AttributeError: can't set attribute8.2 递归调用错误
class RecursiveError:
def __init__(self):
self._value = 0
@property
def value(self):
# ❌ 错误:递归调用自己
return self.value
@property
def correct_value(self):
# ✅ 正确:返回内部属性
return self._value8.3 属性名冲突
class NamingConflict:
def __init__(self):
self.value = 10 # 公共属性
self._value = 20 # 内部属性
@property
def value(self):
# ❌ 这会覆盖实例属性
return self._value
obj = NamingConflict()
print(obj.value) # 20,不是预期的109. 最佳实践
9.1 命名约定
- 内部属性使用下划线前缀:
self._attribute - 属性方法不要加下划线:
@property def attribute(self) - 保持属性名的一致性
9.2 设计原则
- 只在需要额外逻辑时使用属性装饰器
- 简单的属性访问直接使用公共属性
- 提供清晰的错误信息和文档字符串
- 考虑向后兼容性
9.3 性能考虑
- 属性装饰器有轻微的性能开销
- 对于频繁访问的属性,考虑缓存计算结果
- 复杂的验证逻辑可能影响性能
10. 课后练习
练习1:银行账户类
创建一个BankAccount类:
- 使用属性装饰器管理余额
- 存款和取款时验证金额有效性
- 添加账户状态(激活/冻结)控制
- 实现利息计算(计算属性)
练习2:学生成绩管理
创建一个Student类:
- 管理学生的各科成绩
- 使用属性装饰器确保成绩在0-100范围内
- 实现平均分、最高分、最低分等计算属性
- 添加成绩等级评定(A/B/C/D/F)
练习3:配置类
创建一个Configuration类:
- 使用属性装饰器管理配置项
- 支持配置的验证和类型转换
- 实现配置的持久化(保存到文件)
- 添加配置变更的监听器机制
总结
今天我们学习了:
- @property装饰器:将方法转换为只读属性
- @setter装饰器:为属性添加设置和验证逻辑
- @deleter装饰器:定义属性删除时的行为
- 属性装饰器让属性访问更加安全和可控
- 相比传统的getter/setter方法,语法更简洁,可读性更好
记住:属性装饰器是实现封装和数据验证的强大工具,但要合理使用,避免过度设计。
下一集我们将学习抽象类与接口,探索更高级的面向对象设计概念!