在Python面向对象编程里,属性访问控制是保证对象内部数据合法性的重要手段。很多初学者习惯用公开实例变量存储状态,但这会让任何外部代码随意修改数值。property装饰器提供了一条优雅路径:把类里的普通方法变成看似属性的访问入口,从而在读取或写入时插入自定义逻辑。

一、property的基本用法
property本身是一个内置类,也是描述符的一种实现。通过@property装饰器,我们可以将一个getter方法标记为属性;再用@属性名.setter和@属性名.deleter补充赋值与删除行为。调用时无需加括号,就像访问普通字段一样,但背后执行的是函数。
下面这段代码展示了一个最简单的温度类,利用property阻止非法赋值:
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('温度不能低于绝对零度')
self._celsius = value
@celsius.deleter
def celsius(self):
print('删除温度记录')
del self._celsius
t = Temperature(25)
print(t.celsius)
t.celsius = 30
# t.celsius = -300 会抛出ValueError
从示例可见,外部代码写t.celsius = 30时,实际调用了被@celsius.setter修饰的方法。如果直接访问t._celsius虽仍可绕过,但下划线约定提醒使用者这是受保护成员。这种写法比定义get_celsius和set_celsius两套方法直观得多。
property的另一个好处是向后兼容。假设早期版本类直接用公有属性temp,后期需要加校验,只需把temp改为私有_temp并添加property,老代码t.temp的调用方式完全不用改。这对维护大型项目非常友好。
二、底层原理与描述符
理解property不能避开描述符协议。Python中只要类实现了__get__、__set__或__delete__中任意一个方法的对象,就能称为描述符。property类在内部正是用这些魔术方法把函数包装成描述符实例,并绑定到类的属性名上。
当我们访问obj.attr,解释器会先查类型字典里attr是否为描述符,若是且含__get__,就调用type(obj).__dict__['attr'].__get__(obj, type(obj))。property把自己的getter函数存进__get__,setter函数存进__set__,由此完成方法到属性的映射。
# 等价手写描述符,帮助理解property机制
class CelsiusDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value < -273.15:
raise ValueError('温度非法')
instance.__dict__[self.name] = value
class Temp:
celsius = CelsiusDescriptor('_celsius')
def __init__(self, v):
self.celsius = v
temp = Temp(20)
print(temp.celsius)
这段手写描述符和property效果类似,但property语法更简洁,还自动处理了getter与setter的配对。值得注意的是,描述符必须定义在类级别,不能放在实例字典里,否则不会被触发。
另外,property默认只读。如果只写@property而没有setter,外部赋值会报AttributeError。这一特性适合暴露计算值,比如根据宽高算面积,只允许看不允许改。
三、实际场景中的进阶技巧
除了基础校验,property常用于懒加载。有些属性计算开销大,且不一定每次都用,可以第一次访问时算好并缓存到私有变量,之后直接返回。这样既保持接口干净,又提升性能。
下面例子模拟从网络取配置,只用一次请求,后续访问走缓存:
class Config:
def __init__(self):
self._data = None
@property
def data(self):
if self._data is None:
print('模拟加载配置')
self._data = {'version': '1.0', 'debug': False}
return self._data
c = Config()
print(c.data)
print(c.data)
运行后会发现“模拟加载配置”只打印一次,说明第二次读取走了缓存。若用普通方法,调用方容易忘记缓存或重复调用;property把细节封装在内部,调用方无感知。
还可以把property配合@classmethod或继承使用,在子类中覆盖父类属性逻辑。比如基类提供只读id,子类可扩展为带格式化的展示属性。这种组合让类结构清晰,也避免了大量的if判断散落在业务代码里。
四、使用注意与常见误区
一个典型误区是认为property能完全阻止访问私有变量。实际上Python没有硬私有,用户仍可直接读_obj._x。property只是约定加便利,不是安全边界。敏感数据应放在更低层如序列化限制或进程外。
另一个问题是性能。每次访问property都会调用函数,比直接读实例变量稍慢。在极端高频循环里,如果逻辑简单,可考虑用普通属性或__slots__优化。但绝大多数业务场景,这点开销可忽略,代码可读性收益更大。
import time
class A:
def __init__(self):
self.x = 1
@property
def y(self):
return self.x
a = A()
start = time.time()
for _ in range(1000000):
_ = a.x
print('直接属性', time.time() - start)
start = time.time()
for _ in range(1000000):
_ = a.y
print('property', time.time() - start)
上述测试通常显示property稍慢,但百万次差异仅在毫秒级。因此除非性能剖析明确指出此处瓶颈,否则不必过度优化。清晰的对象接口比微小速度提升更重要。
最后提醒,property名字不要和实例变量同名且不带下划线,否则在setter里self.name = value会无限递归。正确做法是内部用_name存储,外部用name做property,这也是Python社区普遍遵循的规范。