前言:
在Python中,property 是一种内置的装饰器,它可以将类的方法转换为属性,让你在不改变类接口的情况下添加额外的逻辑,如输入值的验证、取值的计算等。property 可以作为一种方式让你的类接口保持清晰且易于使用。
property 装饰器最常见的应用场景是将类的属性封装起来,提供getter 和 setter 方法。这种方式的原因是,你可以对属性赋值或者取值的代码进行控制,而不是直接暴露属性。
property 通常会按照下面这样的模式使用:
@property 装饰一个方法,定义属性名,该方法为属性的 “getter” 方法。
@<property_name>.setter 装饰一个方法,将其定义为同名属性的 “setter” 方法。
@<property_name>.deleter 装饰一个方法,将其定义为同名属性的 “deleter” 方法。
1 应用场景:
1.1 数据封装与保护
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/12 23:00
# @Author : Summer
# @File : property_test.py
# @describe:
"""class BankAccount:def __init__(self, initial_balance=0.0):self._balance = initial_balance # 初始化时将_balance设为初识余额@propertydef balance(self):"""获取账户余额。"""return self._balance@balance.setterdef balance(self, value):"""设置账户余额,拒绝直接设置。"""raise ValueError("Cannot directly set balance; please use deposit() or withdraw() methods.")def deposit(self, amount):"""存款方法,只有金额是正数时才接受该交易。"""if amount <= 0:raise ValueError("Deposit amount must be positive.")self._balance += amountdef withdraw(self, amount):"""取款方法,只有余额充足且金额是正数时才接受该交易。"""if amount <= 0:raise ValueError("Withdrawal amount must be positive.")if amount > self._balance:raise ValueError("Insufficient balance.")self._balance -= amount# 使用
account = BankAccount(1000) # 初始余额为1000
print(account.balance) # 输出余额account.deposit(500) # 存款500
print(account.balance) # 输出新余额try:account.balance = 1500 # 尝试直接设置余额,将引发异常
except ValueError as e:print(e)account.withdraw(200) # 取款200
print(account.balance) # 输出新余额try:account.withdraw(2000) # 尝试取款超过余额的金额,将引发异常
except ValueError as e:print(e)
在这个例子中,BankAccount类有一个私有属性_balance,这意味着我们不想让外部直接修改它。我们提供了一个balance属性的getter来获取余额,但我们故意没有提供setter去允许直接修改余额,反而提供了deposit和withdraw方法来合理地修改余额。
当尝试直接设置account.balance时,由于没有适当的setter,property装饰器会引发一个ValueError异常。而当通过deposit和withdraw方法修改余额时,可以在方法内部加入一定的验证逻辑,比如只接受正数金额,以及在取款时确保余额充足等。
通过这种方式,我们可以确保账户余额不会被非法修改,并且所有余额的变化都是通过安全的方法进行的。这就是使用property装饰器进行数据封装和保护的一个典型例子。
1.2 属性计算与逻辑处理
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/12 23:00
# @Author : Summer
# @File : property_test.py
# @describe:
"""class Rectangle:def __init__(self, width, height):self.width = widthself.height = height@propertydef area(self):"""计算矩形的面积。"""return self.width * self.height@propertydef perimeter(self):"""计算矩形的周长。"""return 2 * (self.width + self.height)def resize(self, new_width, new_height):"""修改矩形的尺寸,并自动更新计算属性。"""if new_width <= 0 or new_height <= 0:raise ValueError("Width and height must be positive.")self.width = new_widthself.height = new_height# 使用
rectangle = Rectangle(3, 4)
print(rectangle.area) # 输出面积:12
print(rectangle.perimeter) # 输出周长:14rectangle.resize(5, 6)
print(rectangle.area) # 输出新面积:30
print(rectangle.perimeter) # 输出新周长:22try:rectangle.resize(-3, 6)
except ValueError as e:print(e) # 尺寸必须为正数,否则抛出异常
在这个例子中,Rectangle类有width和height两个属性,以及两个由这两个属性计算得出的只读属性area和perimeter。area属性返回矩形的面积,perimeter属性返回矩形的周长。这两个属性都没有setter方法,因为它们取决于width和height的值,而不是直接设置的。
此外,如果需要修改矩形的尺寸,我们定义了一个名为resize的方法来同时更新width和height。这时,由于area和perimeter的计算依赖于这两个属性,它们也会自动更新,因此我们无需手动同步这些计算属性。
通过使用property,我们可以将计算和逻辑封装在类内部,使得外部接口简洁并易于使用,同时保持内部数据的一致性。这也使得我们能够在未来改变计算属性的内部实现而不影响调用方的代码。
1.3 高级getter和setter用法
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/12 23:00
# @Author : Summer
# @File : property_test.py
# @describe:
""""""使用@property装饰器来定义属性的getter和setter方法,并使用@name.setter和@price.setter来分别定义name和price属性的setter方法。在setter方法中,我们检查传入的值的类型和值是否满足要求,如果满足要求,则将值赋给对应的属性,否则引发异常。
"""class Product:def __init__(self, name, price):self.name = nameself.price = price@propertydef name(self):return self._name@name.setterdef name(self, value):if not isinstance(value, str):raise TypeError("Name must be a string")if not value.strip():raise ValueError("Name cannot be empty")self._name = value@propertydef price(self):return self._price@price.setterdef price(self, value):if not isinstance(value, (int, float)):raise TypeError("Price must be a number")if value <= 0:raise ValueError("Price must be positive")self._price = round(value, 2) # 注意:在此处我们将价格四舍五入到两位小数# 使用
product = Product("Coffee", 3.99)
print(product.name) # 输出商品名称
print(product.price) # 输出商品价格try:product.name = "" # 尝试设置一个空的商品名称,将引发异常
except ValueError as e:print(e)try:product.price = "Free" # 尝试设置一个非数字的价格,将引发异常
except TypeError as e:print(e)product.price = 5.98765 # 此时会自动四舍五入价格
print(product.price)
在这个例子中,Product类有两个属性:name和price。每个属性都有一个getter和setter,而setter方法中包含了输入的验证和转换。
对于name属性,setter方法确保传入的值是字符串,并且非空;
对于price属性,setter方法检查值是否是一个正数 (可以是整数或浮点数) 并且将其四舍五入到两位小数。
这些额外的验证确保了:
商品名称不为空且为文本类型。
商品价格总是正数,并且为了表示金钱它总是以两位小数显示。
通过在setter中包含这些逻辑,我们可以确保Product类的实例始终维持在有效和合理的状态。如果尝试为这些属性赋值无效的数据,类会引发异常,阻止这样做,并且可以为异常处理提供更多上下文。这种模式帮助保持数据的完整性,并可以在属性被错误地设置时立即发现问题。
使用property的好处:
property() 函数是 Python 中用于创建可管理属性的重要工具,它可以实现数据封装、访问控制、属性计算等功能。
封装:property允许开发者隐藏(封装)内部实现的细节,提供一个干净的API。内部变量往往以下划线(如_variable或__variable)作为前缀命名,表示它们不应直接访问。
数据验证:通过使用setter方法,开发者可以轻松加入数据验证逻辑,确保类的状态总是有效的。例如,可以检查属性值是否在预期范围内,或者是否是正确的类型。
计算属性:有些属性的值是基于对象的状态计算得到的。使用getter方法,可以轻松地根据其他属性动态生成这些值。这些属性通常被认为是只读属性。
延迟计算和缓存(惰性计算):对于计算成本较高的属性值,可以在首次访问时进行计算并缓存结果,而不是在对象创建时就计算。这样做可以提高性能,尤其是在该属性不一定会被每个对象使用的情况下。
观察者模式:通过使用property装饰器的setter方法,可以在一个属性被修改时触发某些额外的操作,例如日志记录、通知或是其它形式的状态同步。
保持向后兼容性:如果你的类曾经公开了一个属性,并且你想添加一些对其访问控制而不破坏现有代码,那么你可以将它转变为一个用property装饰器实现的方法,这样做不需要修改对这个属性的引用。