Python 中 property 装饰器代替 getter setter
为什么需要属性访问控制
在面向对象编程中,为了封装对象的内部状态,通常会定义 getter 和 setter 方法来控制属性的读取和赋值。例如,在 Java 中我们习惯这样写:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Python 一开始的设计更倾向于直接访问属性,比如 obj.attr。但如果后期发现需要对属性赋值增加验证逻辑或计算逻辑,直接将公有属性改为方法,会破坏所有调用处的代码。
传统 Python 的做法:手动编写 getter / setter
在引入 property 之前,Python 社区的一种习惯是先用普通属性,然后在需要时改用 get_xxx、set_xxx 方法。
class Person:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, value):
if not value:
raise ValueError("名字不能为空")
self._name = value
使用时需要显式调用方法:
p = Person("Alice")
print(p.get_name()) # Alice
p.set_name("Bob")
这种方式最大的问题是语法变得啰嗦,而且与原有的 p.name 风格不一致,代码可读性下降。
property 内置函数:将方法伪装成属性
property 是一个内置类,可以将访问属性的行为映射到自定义的方法上。语法:
property(fget=None, fset=None, fdel=None, doc=None)
我们可以用 property 来统一接口,让外部依然使用 obj.attr 的形式,但内部执行的是方法。
class Person:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, value):
if not value:
raise ValueError("名字不能为空")
self._name = value
name = property(get_name, set_name)
现在可以像使用普通属性一样访问:
p = Person("Alice")
print(p.name) # 自动调用 get_name
p.name = "Bob" # 自动调用 set_name
代码更简洁,且没有破坏原有接口。
使用 @property 装饰器实现
Python 提供了更优雅的装饰器方式。将读取方法用 @property 装饰,赋值方法用 @<属性名>.setter 装饰。
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
"""姓名属性,存储为 _name"""
return self._name
@name.setter
def name(self, value):
if not value:
raise ValueError("名字不能为空")
self._name = value
这完全等价于前面的 property(get_name, set_name) 模式,但代码组织更清晰,把读、写逻辑放在一起。
只读属性
如果只定义 @property 而不提供 setter,则该属性为只读。
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53975
c.radius = 10 # AttributeError: can't set attribute
在 Circle 中,radius 为只读属性,area 是基于 _radius 动态计算的只读属性,每次访问都会重新计算。
定义删除行为
还可以用 @<属性名>.deleter 在 del obj.attr 时触发特定操作。
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@name.deleter
def name(self):
print(f"删除 {self._name}")
del self._name
p = Person("Alice")
del p.name # 输出:删除 Alice
典型应用场景
1. 数据验证
在 setter 中加入检查逻辑,防止无效数据进入对象。
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
2. 惰性计算与缓存
结合私有属性可以实现惰性加载,只有在第一次访问时才计算,之后返回缓存值。
class DataLoader:
def __init__(self):
self._data = None
@property
def data(self):
if self._data is None:
print("加载数据...")
self._data = [1, 2, 3, 4] # 模拟耗时操作
return self._data
3. 向后兼容重构
旧代码使用公有属性 obj.value,后来需要在赋值时添加副作用(如打日志、更新 UI)。
使用 @property 可以在不修改外部代码的情况下,将普通属性升级为带有逻辑的属性。
4. 暴露 C 风格的结构体接口
在封装底层库时,可以用 property 将复杂的方法调用包装成简单的属性访问,让 API 更直观。
性能与注意事项
@property本质是描述符,相比直接访问属性会有微小的函数调用开销。在性能极其敏感的循环中建议直接使用普通属性。- 避免在
getter中产生耗时计算,如果需要,可结合缓存设计。 - 不要滥用
property隐藏过多业务逻辑,单一职责原则同样适用于属性。 property应该用于“属性”的语义,而不是将复杂运算伪装成属性访问,否则会降低代码可读性。
总结
| 方式 | 代码示例 | 特点 |
|---|---|---|
| 直接属性 | obj.attr |
简单直接,无控制 |
| getter/setter 方法 | obj.get_attr() / obj.set_attr(v) |
有控制,语法啰嗦 |
property() 函数 |
attr = property(get_a, set_a) |
统一为属性访问 |
@property 装饰器 |
@property / @attr.setter |
最优雅,强可读性 |
使用 @property 装饰器可以让你在需要时轻松加入验证、计算、日志等逻辑,同时保持 API 的简洁统一,是 Python 中实现属性访问控制的最佳实践。