怎么在Python中利用property实现只读属性

Leo
Leo
发布时间:2026-10-23| 4人浏览过
原创

在 Python 的面向对象编程中,实现只读属性的核心方法是使用内置的 @property 装饰器:为目标属性定义一个同名的读取方法(Getter),并且不声明对应的修改方法(Setter)。这样一来,外部代码可以像访问普通公开变量一样读取该属性,而一旦尝试直接赋值或修改,Python 解释器会自动拦截并抛出 AttributeError。这种做法既保证了简洁直观的访问语法,又在类的接口层建立了清晰的防篡改边界。

一、核心原理:property 的拦截与分发机制

在了解具体代码写法前,先观察 Python 属性访问的底层行为。普通实例变量是直接存放在对象的 __dict__ 字典中的,任何外部调用者都可以随意覆写。而 property 本质上是 Python 的内置描述符对象(Descriptor)。

当一个方法被 @property 装饰后,该方法就成为了描述符的 getter。如果后续没有显式通过 @<属性名>.setter 注册赋值函数,该描述符就处于“只读”配置状态。当外部发起读取请求时,Python 自动执行该 getter 方法并返回值;当外部发起赋值请求时,描述符的底层 __set__ 机制由于找不到绑定的 setter 实现,会直接判定非法并抛出异常。

怎么在Python中利用property实现只读属性
图 1:property 只读属性的调用分发与异常拦截路径

二、标准三步法:完整实现流程与代码示例

在日常开发中,利用 property 实现只读属性遵循非常标准的三步规程:

  1. 使用受保护命名前缀保存底层数据:在类的 __init__ 构造器中,将实际状态存入带有前导单下划线的变量(例如 self._radius)。根据 PEP 8 约定,单下划线表明这是供内部使用的受保护成员。
  2. 使用 @property 装饰 getter 函数:定义一个与对外公开属性同名的方法,在方法上方标注 @property,内部返回刚才存储的底层变量。
  3. 坚决不要声明对应的 setter:不编写 @<属性名>.setter 装饰的方法。省略这一步就是构建只读属性的决定性动作。
怎么在Python中利用property实现只读属性
图 2:实现只读属性的标准三步流程

下面通过一个完整的几何圆(Circle)类,展示实际的代码编写与执行效果:

class Circle:
    def __init__(self, radius: float):
        # 步骤 1:使用受保护的私有变量存储实际数据
        self._radius = radius

    # 步骤 2:使用 @property 定义 Getter 方法
    @property
    def radius(self) -> float:
        """圆的半径(只读属性)"""
        return self._radius

    # 步骤 3:坚决不写 @radius.setter


# 实例化对象并进行验证
c = Circle(5.0)

# 正常读取属性(语法与普通成员完全一致)
print(f"当前圆的半径为: {c.radius}")

# 尝试对只读属性进行赋值修改
try:
    c.radius = 10.0
except AttributeError as e:
    print(f"成功触发拦截: {type(e).__name__}: {e}")
终端示例输出 当前圆的半径为: 5.0
成功触发拦截: AttributeError: property 'radius' of 'Circle' object has no setter

在 Python 3 中,当试图为缺少 setter 的 property 赋值时,解释器抛出的标准异常信息明确指明:property 'radius' of 'Circle' object has no setter。这直接向调用者声明了该属性的只读属性约束。

三、进阶应用:无底层状态的“动态计算型”只读属性

只读属性除了用于保护已存在的私有变量外,另一个更具生产价值的场景是实现动态派生属性。这类属性在类内部根本不需要额外的字段来存储,而是完全基于其他基础属性实时计算得出。

例如,圆的面积(area)或周长(perimeter)完全由半径决定。如果用单独的属性存储面积,每次修改半径时都得手动同步面积,极易产生状态不一致;而如果使用普通方法 c.get_area(),调用端又需要写小括号。使用只读属性即可兼顾“实时一致性”与“字段式简洁访问”:

import math

class Circle:
    def __init__(self, radius: float):
        if radius <= 0:
            raise ValueError("半径必须为正数")
        self._radius = radius

    @property
    def radius(self) -> float:
        return self._radius

    @property
    def area(self) -> float:
        """动态计算面积,纯只读"""
        return math.pi * (self._radius ** 2)

    @property
    def perimeter(self) -> float:
        """动态计算周长,纯只读"""
        return 2 * math.pi * self._radius


c = Circle(3.0)
print(f"半径: {c.radius}")
print(f"计算所得面积: {c.area:.2f}")
print(f"计算所得周长: {c.perimeter:.2f}")

try:
    c.area = 50.0
except AttributeError as err:
    print(f"拦截修改面积: {err}")
终端示例输出 半径: 3.0
计算所得面积: 28.27
计算所得周长: 18.85
拦截修改面积: property 'area' of 'Circle' object has no setter

动态只读属性不仅消除了脏数据同步的隐患,而且即使未来内部计算公式发生调整,外部调用代码的访问方式依然保持不变,具备极佳的接口向下兼容性。

四、深度警惕:可变对象引发的“假只读”陷阱与防御

这是许多初中级开发者最容易忽视的关键陷阱:@property 的只读保护仅作用于属性名称的“重绑定”(Rebinding),而不作用于对象本身的“就地修改”(Mutation)。

如果只读属性返回的是一个不可变对象(如 int、float、str、tuple 等),那么它从内到外都是坚固的;但如果属性返回的是一个可变容器(如 list、dict、set),外部调用者虽然无法执行 obj.tags = [...],却可以通过 obj.tags.append(...) 或 obj.tags.pop() 轻易修改其内部内容,从而直接破坏类内部的封装!

怎么在Python中利用property实现只读属性
图 3:不可变类型与可变类型的只读安全性对比与防御方案

漏洞示范与防御代码实现

以下代码对比了“未设防的假只读”与“防御性拷贝/不可变封装”的实际差异:

class UserManagerUnsafe:
    def __init__(self, roles: list):
        self._roles = list(roles)

    @property
    def roles(self) -> list:
        # 漏洞所在:直接返回了内部真实列表的引用
        return self._roles


# 演示被绕过的脆弱性
u1 = UserManagerUnsafe(["user"])
# u1.roles = ["admin"]  # 这一行确实会被 property 拦截
u1.roles.append("admin")  # 但是直接操作内部列表,成功绕过了只读保护!
print(f"漏洞暴露:内部 roles 被恶意追加为: {u1.roles}")


# ---------------- 严谨防御方案 ----------------

class UserManagerSafe:
    def __init__(self, roles: list):
        self._roles = list(roles)

    @property
    def roles(self) -> tuple:
        """方案一:转换为不可变类型(推荐)"""
        return tuple(self._roles)

    @property
    def roles_copy(self) -> list:
        """方案二:返回浅拷贝副本"""
        return self._roles.copy()


u2 = UserManagerSafe(["user"])
try:
    # tuple 没有 append 方法,试图篡改会立即报错
    u2.roles.append("admin")
except AttributeError as err:
    print(f"防御成功 (不可变元组): {err}")

# 修改副本不会对对象内部产生任何污染
fake_list = u2.roles_copy
fake_list.append("hacker")
print(f"内部真实数据依然纯净: {u2.roles}")
终端示例输出 漏洞暴露:内部 roles 被恶意追加为: ['user', 'admin']
防御成功 (不可变元组): 'tuple' object has no attribute 'append'
内部真实数据依然纯净: ('user',)
选型建议

当只读属性涉及列表或集合时,优先将其转换为 tuple 或 frozenset 返回。如果需要字典只读,可以使用标准库 types.MappingProxyType 提供只读映射视图,既杜绝内存重复分配,又能阻断任何写入。

五、底层原理深入:为什么缺少 Setter 就会抛出异常?

为了在团队技术评审或排查复杂 Bug 时做到心中有数,我们需要理解 @property 在 Python 解释器底层的运转模型。

在 Python 中,任何包含 __get__、__set__ 或 __delete__ 特殊方法的类都被称为“描述符”。property 是一个典型的内置“数据描述符”(Data Descriptor)。当解释器在实例上执行赋值语句(例如 obj.x = value)时,其解析优先级如下:

  1. 首先检查 obj 的所属类(以及基类)的 __dict__ 中是否存在同名属性,且该属性是否为数据描述符(即定义了 __set__)。
  2. 如果存在且是数据描述符,优先调用描述符的 __set__ 方法,而完全忽略实例自己的 __dict__。
  3. 在 property 对象的 C 源码实现中,其内部维护了指向 fget、fset、fdel 三个函数的指针。当我们没有显式编写 @prop.setter 时,fset 指针的值为 NULL(或 None)。
  4. 当描述符的 __set__ 被调用时,它首先检查 fset 是否可用。一旦发现 fset == NULL,它便断定该属性不可写,立即通过 PyErr_SetString(PyExc_AttributeError, "can't set attribute") 抛出异常。

这解释了一个有趣的现象:即便你在外部强行尝试 obj.__dict__['radius'] = 999 将数值直接注入实例字典,当你再次执行 obj.radius 时,由于数据描述符的读取优先级高于实例字典,返回的依然是 @property getter 方法计算后的结果,这为数据完整性提供了极为强韧的底层保障。

六、决策树与生命周期全景总结

为了帮助读者在日常编码时迅速作出正确的技术选型,下图总结了只读属性的完整设计决策路径:

怎么在Python中利用property实现只读属性
图 4:Python 只读属性设计决策树与检查清单

实现方案与场景速查表

需求场景 推荐实现方式 异常行为 防御与注意事项
固定值/配置保护(如数据库连接串、UUID) @property + self._id 赋值时抛出 AttributeError 无需特殊处理,不可变基础类型天然安全
动态派生计算(如圆面积、全名拼接) @property 内部实时 return a + b 赋值时抛出 AttributeError 不占用内存字段,完全避免状态不一致
只读列表/集合(如用户角色列表、标签池) @property + return tuple(self._items) 尝试 append/pop 抛出 AttributeError 绝不能直接返回内部 list 引用,务必转为元组
只读字典映射(如常量键值表、环境配置) MappingProxyType(self._dict) 赋值或修改键值抛出 TypeError 零拷贝提供动态只读视图,阻断修改操作

总结而言,在 Python 中构建只读属性是一个优雅而轻量的过程:只需使用 @property 定义 getter 函数并刻意忽略 setter 即可。在面对不可变类型时,这一层防护坚不可摧;在面对可变容器时,结合不可变转换或防御性拷贝,便能构建出健壮、严密且符合 Pythonic 风格的高质量面向对象接口。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。