使用 @property
@property 可以把一个方法包装成“像属性一样使用”的接口。调用方写 account.balance,类内部仍然可以执行计算、校验、转换或访问其他存储。这样既保留了点号语法的简洁,又不会把实现细节直接暴露给外部代码。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 使用 getter、setter 和 deleter 定义属性行为;
- 区分只读属性、可读写属性和计算属性;
- 在 setter 中维护对象不变量,避免递归赋值;
- 理解 property 与描述器、继承和抽象基类的关系;
- 判断什么时候应该使用普通属性、property 或方法。
先看一个完整例子
下面的订单对象把金额统一保存为“分”,对外仍提供元为单位的属性。读取时格式化订单摘要,赋值时自动校验并转换输入:
class Order:
def __init__(self, customer: str, total_yuan: float):
self.customer = customer
self.total_yuan = total_yuan
@property
def total_yuan(self) -> float:
"""以元为单位返回金额。"""
return self._total_cents / 100
@total_yuan.setter
def total_yuan(self, value: float | str) -> None:
if isinstance(value, bool):
raise TypeError("订单金额不能是布尔值")
try:
amount = float(value)
except (TypeError, ValueError) as error:
raise TypeError("订单金额必须是数字") from error
if amount < 0:
raise ValueError("订单金额必须是非负数")
self._total_cents = round(amount * 100)
@property
def summary(self) -> str:
return f"{self.customer}:¥{self.total_yuan:.2f}"
order = Order("小林", 19.9)
print(order.total_yuan) # 19.9
order.total_yuan = "29.90"
print(order.summary) # 小林:¥29.90
# order.total_yuan = -1 # ValueError
调用方不需要知道金额实际保存在 _total_cents 中,也不需要手动调用 get_total() 或 set_total()。setter 还是对象创建时的统一入口,因此初始化和后续修改遵守同一套规则。
property 的基本结构
只读属性
只定义 getter 时,外部可以读取属性,但不能给它赋值:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
rectangle = Rectangle(3, 4)
print(rectangle.area) # 12
# rectangle.area = 20 # AttributeError: property 'area' ... has no setter
area 没有单独保存一份值,而是在读取时计算。宽度或高度改变后,面积自然保持最新。只读并不等于绝对不可变:如果类提供了改变 width、height 的接口,area 的结果仍会变化。
可读写属性
在同名 property 方法后使用 @属性名.setter,可以定义赋值行为:
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = float(value)
temperature = Temperature(20)
temperature.celsius = 25
print(temperature.celsius) # 25.0
setter 通常做三件事:检查类型或范围、把输入规范化成内部类型、写入内部存储。校验失败时应尽早抛出 TypeError 或 ValueError,不要让对象进入无法解释的状态。
删除属性
只有业务上确实存在“删除”的含义时,才定义 deleter:
class CacheEntry:
def __init__(self, value: str | None):
self._value = value
@property
def value(self) -> str | None:
return self._value
@value.deleter
def value(self) -> None:
self._value = None
entry = CacheEntry("结果")
del entry.value
print(entry.value) # None
如果删除会破坏对象不变量,宁可不提供 deleter,让 del entry.value 直接失败,也不要定义含义模糊的删除操作。
装饰器语法等价于 property()
下面两种写法效果相同:
class User:
def __init__(self, name: str):
self._name = name
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if not value.strip():
raise ValueError("姓名不能为空")
self._name = value.strip()
class User:
def __init__(self, name: str):
self._name = name
def get_name(self) -> str:
return self._name
def set_name(self, value: str) -> None:
if not value.strip():
raise ValueError("姓名不能为空")
self._name = value.strip()
name = property(get_name, set_name)
property(fget=None, fset=None, fdel=None, doc=None) 接收读取、写入、删除函数和文档字符串。装饰器版本通常更易读,显式调用 property() 适合需要动态组装属性或阅读旧代码时使用。
@property 装饰的方法名决定了公开属性名;setter 必须装饰同一个 property:
class Score:
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, number: int) -> None:
self._value = number
如果误写成 @property 修饰了另一个名字,或者把 setter 赋给了不同名称,就不会得到预期的属性接口。
设计 getter:返回什么、何时计算
计算属性不应保存重复状态
当一个值可以由其他状态稳定推导出来时,优先实时计算:
class Cart:
def __init__(self, prices: list[float]):
self.prices = list(prices)
@property
def total(self) -> float:
return sum(self.prices)
如果同时保存 self._total 和 self.prices,每次价格变化都必须同步两个值,容易产生过期数据。只有计算成本很高、读取频率远高于修改频率时,才考虑缓存,并明确缓存失效策略。
property 不一定比方法更合适
适合 property 的读取通常是轻量、无副作用、看起来像对象状态的操作,例如 area、full_name、is_expired。以下场景通常应该使用方法:
- 操作需要参数,例如
convert_to("USD"); - 操作可能执行 I/O、网络请求或昂贵计算;
- 操作会修改状态或产生明显副作用;
- 调用方需要知道这是一次动作而不是读取状态。
不要因为语法简短,就把发送邮件、查询数据库之类的动作伪装成 user.email。
返回可变对象时注意封装
只读 property 不能阻止调用方修改它返回的列表:
class Playlist:
def __init__(self, songs: list[str]):
self._songs = list(songs)
@property
def songs(self) -> list[str]:
return self._songs
playlist = Playlist(["A", "B"])
playlist.songs.append("C") # 绕过了类的校验
如果只允许读取,可以返回元组或副本:
class Playlist:
def __init__(self, songs: list[str]):
self._songs = list(songs)
@property
def songs(self) -> tuple[str, ...]:
return tuple(self._songs)
def add(self, song: str) -> None:
if not song.strip():
raise ValueError("歌曲名不能为空")
self._songs.append(song.strip())
返回元组表达不可变视图,返回副本则允许调用方自由修改副本。两者都不会泄露内部列表的可变引用,但副本会有额外的复制成本。
setter 的校验、转换与不变量
不要在 setter 中给同名属性赋值
setter 应写入内部名称,例如 _celsius,而不是再次写入 self.celsius:
class Correct:
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, number: int) -> None:
self._value = number
class Recursive:
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, number: int) -> None:
# self.value = number # 再次触发 setter,最终 RecursionError
self._value = number
这是使用 property 最常见的错误之一。内部存储可以采用单下划线名称、__name 名称改写,或其他明确的后端存储,但不要与 property 同名。
保持跨字段不变量
有些规则涉及多个字段,应在一个公共入口中一起校验:
class DateRange:
def __init__(self, start: int, end: int):
self._start = 0
self._end = 0
self._set_range(start, end)
@property
def start(self) -> int:
return self._start
@property
def end(self) -> int:
return self._end
def _set_range(self, start: int, end: int) -> None:
if start > end:
raise ValueError("起点不能晚于终点")
self._start, self._end = start, end
如果分别提供 start.setter 和 end.setter,其中一次赋值可能暂时破坏“start 不大于 end”的规则。此时可以提供 move() 这样的整体操作,或者在每次 setter 中完整验证新的组合状态。
property 与继承
子类可以覆盖 getter 或增加 setter
property 是类属性,子类可以重写它:
class Document:
@property
def title(self) -> str:
return "未命名"
class Report(Document):
def __init__(self, title: str):
self._title = title
@property
def title(self) -> str:
return self._title
如果父类提供只读 property,子类可以重新声明同名 property 并添加 setter,但应确认这不会违反父类原本对调用方的承诺。把只读接口突然变成可写接口通常是兼容的扩展;改变读取结果或异常约定则需要谨慎。
使用 super() 复用父类 property
重写 setter 时,如果还需要父类的校验,可以通过 super() 获取父类 property:
class Person:
def __init__(self, name: str):
self.name = name
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
value = value.strip()
if not value:
raise ValueError("姓名不能为空")
self._name = value
class Employee(Person):
@Person.name.setter
def name(self, value: str) -> None:
value = value.strip()
if len(value) > 30:
raise ValueError("姓名不能超过 30 个字符")
# 追加规则通过后,再复用父类的非空校验和存储逻辑
Person.name.fset(self, value)
在简单场景中,直接使用 @Person.name.setter 最清晰。若继承层次复杂,应该用最小示例测试 property 的覆盖顺序,并避免依赖内部的 fget、fset 实现细节。
抽象 property
抽象基类可以要求所有子类提供某个属性。组合装饰器时,先写 @property,再写 @abstractmethod:
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float:
"""返回图形面积。"""
class Square(Shape):
def __init__(self, side: float):
self.side = side
@property
def area(self) -> float:
return self.side**2
# Shape() # TypeError:抽象 property 未实现
print(Square(3).area) # 9
如果抽象属性还必须可写,可以在基类中同时声明抽象 getter 和 setter;子类需要实现完整的读写契约,而不是只实现 getter。
property 的底层:描述器
property 是一种内置描述器。类属性只要实现 __get__()、__set__() 或 __delete__(),就能参与点号访问协议:
class LoggedValue:
def __set_name__(self, owner, name: str) -> None:
self.name = name
self.storage_name = f"_{name}"
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.storage_name, None)
def __set__(self, instance, value) -> None:
print(f"设置 {self.name} = {value!r}")
setattr(instance, self.storage_name, value)
class Settings:
theme = LoggedValue()
settings = Settings()
settings.theme = "dark"
print(settings.theme) # dark
学习 property 时不必马上手写描述器;只有当同一套属性逻辑需要复用到多个字段,或需要控制类创建与实例属性访问时,才考虑实现描述器。property 是更直接、更易读的单字段方案。
常见误区
把 property 当作安全机制
Python 的 property 主要用于接口设计和状态校验,不是权限边界。调用方仍可以访问 _value,也可以通过反射、调试器或修改类来绕过约定。真正的安全控制应放在权限验证、数据访问层和输入边界。
在 getter 中产生意外副作用
用户看到 report.summary 通常会认为这只是读取。如果 getter 修改状态、写文件或发网络请求,调用时机就变得不可预测。把动作写成方法可以让成本和副作用更明显。
为每个字段机械地添加 property
没有校验、转换或计算逻辑时,直接使用公开属性往往更简单:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
如果未来需要校验,通常可以把内部字段改为 _x,再增加 x property。点号调用代码不需要改变,这正是 property 的演进价值。
忽略文档和类型注解
property 是公开 API,应说明单位、返回值、是否允许赋值以及可能抛出的异常。类型注解不能替代运行时校验,但可以帮助 IDE、类型检查器和阅读者理解契约。
什么时候使用 @property
可以用下面的判断快速选择:
- 它是否表现得像对象的状态,而不是一个动作?是,优先考虑 property。
- 读取是否轻量、无明显副作用?不是,考虑方法。
- 是否需要在赋值时校验、转换或维护不变量?是,使用 setter。
- 是否只是简单字段转发,没有任何行为?普通公开属性可能更清楚。
- 是否需要同一套描述器逻辑应用于许多字段?考虑自定义描述器,而不是复制大量 property。
小结
@property把方法包装成自然的点号属性,适合计算、校验和兼容接口演进;- 只写 getter 得到只读属性,使用
@name.setter和@name.deleter添加写入、删除行为; - setter 应写入不同名的内部存储,避免
self.name = ...触发无限递归; - 计算属性不要保存重复状态,返回可变对象时要防止内部引用泄露;
- property 可以被继承、覆盖,也可以与抽象基类组合;
- property 本质上是描述器,但大多数单字段封装不需要手写描述器;
- 没有行为的字段不必机械地使用 property,接口应保持简单。