Python 中 property 装饰器代替 getter setter

FreeGuideOnline 最新 2026-07-06

为什么需要属性访问控制

在面向对象编程中,为了封装对象的内部状态,通常会定义 gettersetter 方法来控制属性的读取和赋值。例如,在 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_xxxset_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 动态计算的只读属性,每次访问都会重新计算。

定义删除行为

还可以用 @<属性名>.deleterdel 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 中实现属性访问控制的最佳实践。