属性装饰器
属性装饰器(@property)将方法调用伪装成属性访问,使得在不影响外部接口的前提下,为数据读写添加计算逻辑、校验规则或访问控制。它是 Python 实现封装的核心工具之一。
@property 基础
在方法前加上 @property 装饰器,即可像访问属性一样读取方法的返回值,无需括号调用。
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""半径,只读属性"""
return self._radius
@property
def area(self):
"""面积,计算属性"""
import math
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5 —— 像属性一样访问
print(c.area) # 78.54... —— 每次访问都重新计算
# c.radius = 10 # AttributeError: can't set attribute
@property 的本质是将方法转换为描述符(descriptor)。当通过实例访问该名称时,解释器自动调用被装饰的方法并返回结果。由于外部无法直接赋值,这实现了只读属性。
@setter 与 @deleter
为 property 添加写控制,需要定义同名方法并使用 @name.setter 装饰器。同理,@name.deleter 控制删除行为。
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 30 # 通过 setter 赋值
print(t.fahrenheit) # 86.0
t.fahrenheit = 98.6 # 反向通过华氏度设置
print(t.celsius) # 37.0
# t.celsius = -300 # ValueError
注意 fahrenheit 的 setter 内部调用了 self.celsius = ...,这会触发 celsius 的 setter,从而复用绝对零度的校验逻辑。这种链式设计避免了重复代码。
如果定义了 setter 但未定义 deleter,尝试 del obj.attr 会抛出 AttributeError。若需要支持删除,应提供 deleter:
class Config:
def __init__(self):
self._timeout = 30
@property
def timeout(self):
return self._timeout
@timeout.setter
def timeout(self, value):
self._timeout = value
@timeout.deleter
def timeout(self):
self._timeout = None
c = Config()
del c.timeout
print(c.timeout) # None
property() 函数形式
@property 装饰器是内置函数 property(fget=None, fset=None, fdel=None, doc=None) 的语法糖。在需要动态创建属性或兼容旧代码时,可以直接使用函数形式。
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
def get_width(self):
return self._width
def set_width(self, value):
if value <= 0:
raise ValueError("width must be positive")
self._width = value
width = property(get_width, set_width, doc="矩形宽度")
r = Rectangle(4, 5)
print(r.width) # 4
r.width = 10 # 调用 set_width
print(r.width) # 10
函数形式在元编程和框架代码中更为常见,因为它允许在运行时组装 getter、setter 和 deleter。
计算属性与缓存
计算属性在每次访问时执行方法体。如果计算成本较高且结果不会频繁变化,可以结合缓存策略优化。
class Polygon:
def __init__(self, sides):
self._sides = list(sides)
self._perimeter = None
@property
def sides(self):
return self._sides
@sides.setter
def sides(self, value):
self._sides = list(value)
self._perimeter = None # 缓存失效
@property
def perimeter(self):
if self._perimeter is None:
self._perimeter = sum(self._sides)
return self._perimeter
p = Polygon([3, 4, 5])
print(p.perimeter) # 12 —— 首次计算并缓存
print(p.perimeter) # 12 —— 直接返回缓存值
p.sides = [5, 5, 5]
print(p.perimeter) # 15 —— 缓存已失效,重新计算
Python 3.8+ 提供了 functools.cached_property,自动处理缓存和失效,但要求实例是可哈希的或缓存存储在实例上。对于更复杂的场景,手动管理缓存往往更清晰可控。
只读属性的实现模式
实现只读属性有多种方式,应根据具体需求选择:
模式一:纯 property,无 setter
class ImmutablePoint:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
模式二:setter 中抛出异常
class IDCard:
def __init__(self, id_number):
self._id = id_number
@property
def id(self):
return self._id
@id.setter
def id(self, value):
raise AttributeError("ID is read-only")
模式三:使用 setattr 拦截
class Frozen:
def __init__(self, value):
super().__setattr__('_locked', False)
self.value = value
super().__setattr__('_locked', True)
def __setattr__(self, name, value):
if getattr(self, '_locked', False):
raise AttributeError(f"Cannot modify {name}")
super().__setattr__(name, value)
模式一最简洁,是首选方案。模式二在需要明确告知调用者"此属性不可写"时使用。模式三用于需要全面冻结实例的场景。
property 与描述符的关系
property 是 Python 描述符协议的高层次封装。描述符是实现了 __get__、__set__ 或 __delete__ 方法的类。property 内部正是通过这三个方法实现属性拦截的。
class Validator:
"""自定义描述符,功能类似 property + setter"""
def __init__(self, min_value, max_value):
self.min_value = min_value
self.max_value = max_value
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name, None)
def __set__(self, instance, value):
if not (self.min_value <= value <= self.max_value):
raise ValueError(f"{self.name} must be in [{self.min_value}, {self.max_value}]")
setattr(instance, self.storage_name, value)
class Person:
age = Validator(0, 150)
score = Validator(0, 100)
def __init__(self, age, score):
self.age = age
self.score = score
p = Person(25, 88)
print(p.age) # 25
# p.age = 200 # ValueError: age must be in [0, 150]
当 @property 无法满足需求(例如需要在多个属性间复用同一套逻辑)时,自定义描述符是更优雅的解决方案。Python 3.6 引入的 __set_name__ 让描述符能够自动获知被绑定的属性名,进一步简化了代码。