Python property怎么实现只读属性

一只小鹿
一只小鹿
发布时间:2026-10-31 | 7人浏览过
原创

本文深入讲解在 Python 中如何使用 @property 装饰器实现只读属性,包括标准操作步骤、底层描述符拦截机制、方案对比以及可变对象的深层只读防坑指南。

Python 基础进阶 面向对象设计 属性封装 语言特性

在 Python 中实现只读属性,最标准且直接的做法是使用内置的 @property 装饰器修饰一个方法,同时不定义对应的 @<属性名>.setter 方法。通过这种方式,外部代码可以像访问普通公开变量一样使用点号语法 obj.attribute 读取属性值,而一旦尝试对其执行赋值操作(如 obj.attribute = value),Python 解释器就会主动抛出 AttributeError,从而在语法和运行期层面锁定该属性的只读状态。

Python property怎么实现只读属性
图 1:Python @property 只读属性的核心机制与表现总览

一、只读属性的标准实现步骤

要在一个类中完整落地只读属性,通常遵循清晰的四个阶段:封装受保护的内部状态、声明公开访问方法、使用 @property 标记该方法、并在实例上进行读取与写入拦截验证。

Python property怎么实现只读属性
图 2:实现只读属性的标准四步推导流程

1. 完整代码示例

以经典的几何圆类 Circle 为例,假设圆的半径在初始化后不允许被外部代码随意篡改:

class Circle:
    def __init__(self, radius: float):
        # 步骤 1:使用单下划线前缀存储内部数据(约定为受保护变量)
        self._radius = radius

    # 步骤 2 & 3:使用 @property 装饰器声明公开属性名,且不提供 setter
    @property
    def radius(self) -> float:
        """获取圆的半径(只读属性)"""
        return self._radius

    @property
    def area(self) -> float:
        """动态计算圆的面积(计算型只读属性)"""
        import math
        return math.pi * (self._radius ** 2)

2. 运行结果验证

在 Python 交互式环境或执行脚本中运行如下调用代码:

# 实例化对象
c = Circle(5.0)

# 【验证 1:正常读取】调用方式与普通实例变量完全一致,使用原生点号语法
print("当前半径:", c.radius)  # 输出示例: 当前半径: 5.0
print("计算面积:", c.area)    # 输出示例: 计算面积: 78.53981633974483

# 【验证 2:尝试赋值修改】
c.radius = 10.0

当执行到赋值语句时,解释器会立即中断执行并输出如下异常提示(示例输出):

Traceback (most recent call last):
  File "test.py", line 18, in <module>
    c.radius = 10.0
AttributeError: property 'radius' of 'Circle' object has no setter
版本提示
在 Python 3.11 及更新版本中,报错信息具体为 property 'radius' of 'Circle' object has no setter;而在更早的 Python 3.x 版本中,报错信息通常为 can't set attribute。两者本质相同,均表示底层未绑定 setter 方法。

二、深入底层:Python 如何阻止写入

很多开发者会好奇:为什么仅仅省略 .setter,Python 就能精准拦截写入,甚至连向实例字典 c.__dict__['radius'] 插入数据的常规行为都会被阻断?这背后的核心原理是 Python 的描述符协议(Descriptor Protocol)。

Python property怎么实现只读属性
图 3:Python 数据描述符协议与只读属性分流拦截机制

1. property 的本质是数据描述符

property 是 Python 的内置类,它实现了描述符协议中的两个核心方法:__get__() 和 __set__()。在 Python 的属性查找规则中,同时实现了 __get__ 与 __set__ 的类被称为数据描述符(Data Descriptor)。

数据描述符在属性查找链路中拥有极高的优先级:当执行 obj.name = value 时,解释器优先检查类中是否存在名为 name 的数据描述符。如果存在,解释器会无条件调用该描述符的 __set__() 方法,而完全不会直接操作实例的 __dict__。

2. 缺省 setter 时的拦截逻辑

当通过 @property 装饰一个方法时,相当于在类命名空间中创建了一个 property 实例:

# @property def radius(self): ... 的等价底层写法
radius = property(fget=radius)

此时,该 property 对象的 fset 函数指针为 None。当用户执行 c.radius = 10 时,Python 触发描述符的 __set__ 方法,其内部检查到 fset is None,便直接抛出 AttributeError。正是这种机制确保了只读属性在语言层面的严密性。

三、方案对比:普通公开属性 vs @property 只读属性

在类的属性设计过程中,选择直接暴露属性还是使用 @property 封装只读属性,对代码的维护性和健壮性有着截然不同的影响。

Python property怎么实现只读属性
图 4:普通公开属性与 @property 只读属性的设计方案对比

通过具体对比可以发现两者在工程实践中的核心差异:

  • 状态保护能力:普通属性允许任何外部代码直接覆盖,哪怕传入了非法类型或负数;而 @property 只读属性在入口处切断了外部直接赋值的途径,维护了对象内部状态的完整性。
  • 动态计算能力:普通属性必须在初始化或状态变化时显式计算并存入内存;而只读属性非常适合用于衍生计算字段(如面积、全名、状态判断等)。每次读取都会即时根据基准数据返回,保证强一致性,且避免冗余存储。
  • 调用语法一致性:相较于 Java 或 C++ 中常见的 get_radius() 方法,Python 的 @property 允许调用者沿用简洁明了的 obj.radius 语法,既保持了面向对象封装的安全性,又兼顾了 Python 风格的简洁与自然。

四、避坑指南:不可变性与边界细节

虽然 @property 能够有效阻止直接赋值,但在实际开发中有几个关键边界和常见陷阱需要特别留意:

Python property怎么实现只读属性
图 5:只读属性要点全景与常见陷阱防御方案

1. 警惕可变对象(列表、字典等)的深层穿透

如果只读属性返回的是一个可变对象(如 list、dict 或 set),外部调用者虽然无法直接替换该属性(例如 user.tags = [] 会报错),但可以通过可变对象自带的方法修改其内部数据:

class User:
    def __init__(self, tags):
        self._tags = tags

    @property
    def tags(self):
        return self._tags

u = User(["python", "backend"])
# u.tags = ["new"] 会报错,被正确拦截!
# 但以下操作能够成功执行,破坏了“只读”意图:
u.tags.append("database")  
print(u.tags)  # 输出: ['python', 'backend', 'database']
防御方法
若需要实现深度只读,在 getter 中切勿直接返回可变对象的原引用:
  • 列表场景:转换为不可变元组返回 return tuple(self._tags);
  • 字典场景:使用标准库 types.MappingProxyType(self._dict) 返回只读映射视图。

2. Python 哲学的“防君子不防小人”

Python 中没有类似 C++ 或 Java 那种编译期强制不可见的 private 关键字。单下划线 _radius 是一种强烈的社区约定,明确告知使用者“这是内部属性,外部不应直接访问”。

即使外部代码执意通过 c._radius = 100 强制修改,解释器也不会阻拦。使用 @property 的核心目的是规范公共 API 接口、防止团队成员或调用方无意误改、并提供干净的数据只读语义,而非用于对抗恶意的内存探测或逆向篡改。

3. 删除属性的行为

与赋值操作类似,如果未定义 @<name>.deleter,当外部尝试使用 del obj.radius 删除只读属性时,Python 同样会抛出 AttributeError: can't delete attribute,确保属性无法被意外注销。

核心总结

实现 Python 只读属性的标准实践可以概括为:内部变量加下划线,外部方法加 @property,坚决省略 setter。配合数据描述符的优先级机制,这套方案既拥有优雅的原生点号访问体验,又能为类对象筑起可靠的状态保护墙。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表