第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.age

1.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方法,输出:30

2. @property装饰器基础

2.1 基本语法

class ClassName:
    @property
    def property_name(self):
        # getter逻辑
        return self._attribute_name

2.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 attribute

3. @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 = value

3.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_name

4.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.0

6.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.age

7.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 attribute

8.2 递归调用错误

class RecursiveError:
    def __init__(self):
        self._value = 0
    
    @property
    def value(self):
        # ❌ 错误:递归调用自己
        return self.value
    
    @property
    def correct_value(self):
        # ✅ 正确:返回内部属性
        return self._value

8.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,不是预期的10

9. 最佳实践

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方法,语法更简洁,可读性更好

记住:属性装饰器是实现封装和数据验证的强大工具,但要合理使用,避免过度设计

下一集我们将学习抽象类与接口,探索更高级的面向对象设计概念!

« 上一篇 静态方法与类方法 下一篇 » 抽象类与接口