
本文详解如何用 python 描述符正确实现真正可初始化、不可篡改的只读属性,并解决底层可变对象(如列表)被意外修改的问题。
本文详解如何用 python 描述符正确实现真正可初始化、不可篡改的只读属性,并解决底层可变对象(如列表)被意外修改的问题。
在 Python 中,@property 是实现只读属性最直观的方式,但当项目中存在大量类似字段(如配置类、数据容器、栈结构等)时,重复编写 @property 会显著降低可维护性。此时,使用描述符(Descriptor) 封装只读逻辑是更优雅、更符合 DRY 原则的方案。然而,直接照搬 property 的语义到描述符中容易踩坑——典型问题就是:初始化失败(__set__ 在 __init__ 中即被调用并抛出异常),以及可变对象内容仍可被原地修改(如 obj.items.append(x))。
✅ 正确的只读描述符实现
关键在于:允许首次赋值(即初始化),但禁止后续覆写。这需要在 __set__ 中判断目标属性是否已存在于实例字典中:
class ReadOnly:
def __init__(self, default=None):
self.default = default
def __set_name__(self, owner, name):
# 使用私有名称存储真实值,避免与描述符同名冲突
self._attr_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
# 若未初始化,返回默认值(可选)
return getattr(instance, self._attr_name, self.default)
def __set__(self, instance, value):
# 仅当该私有属性尚未存在时才允许设置(即仅初始化阶段)
if hasattr(instance, self._attr_name):
raise AttributeError(f"Cannot modify read-only attribute '{self._attr_name[1:]}'")
setattr(instance, self._attr_name, value)
def __delete__(self, instance):
raise AttributeError(f"Cannot delete read-only attribute '{self._attr_name[1:]}'")? 注意:
__set_name__中将存储名设为f"_{name}"是关键设计——它确保描述符本身(类属性)与实例实际数据(如_n、_top)完全隔离,避免 descriptor 协议被绕过或覆盖。
使用示例:
立即学习“Python免费学习笔记(深入)”;
class Stack:
S = ReadOnly()
n = ReadOnly()
top = ReadOnly()
def __init__(self, n):
self.S = [None] * n # ✅ 首次赋值成功
self.n = n # ✅
self.top = -1 # ✅
def push(self, x):
if self.top + 1 < self.n:
self.top += 1
self.S[self.top] = x # ✅ 允许修改列表内容(这是预期行为)
s = Stack(4)
print(s.S) # [None, None, None, None]
print(s.top) # -1
# ❌ 以下均抛出 AttributeError
# s.S = [1, 2]
# s.top = 10
# del s.n⚠️ 关于“可变对象内容不可修改”的常见误解
描述符控制的是属性绑定关系(即 obj.attr = value),而非对象内部状态。因此:
-
s.S = [...]→ 被ReadOnly.__set__拦截 ✅ -
s.S.append(...)或s.S[0] = ...→ 直接操作列表对象,与描述符无关 ❌
若业务场景要求彻底冻结可变对象内容(例如禁止任何元素增删改),需额外封装不可变视图。推荐方案如下:
方案一:返回 tuple(适用于只读序列)
def __get__(self, instance, owner):
if instance is None:
return self
raw = getattr(instance, self._attr_name)
return tuple(raw) if isinstance(raw, list) else raw方案二:使用 types.MappingProxyType(适用于 dict)
from types import MappingProxyType # 在 __get__ 中返回 MappingProxyType(raw)
方案三:自定义只读包装器(推荐用于复杂场景)
class ReadOnlyList:
def __init__(self, data):
self._data = list(data) # 深拷贝或只读引用,按需选择
def __getitem__(self, i): return self._data[i]
def __len__(self): return len(self._data)
def __iter__(self): return iter(self._data)
def __repr__(self): return f"ReadOnlyList({self._data!r})"
# 显式禁用所有修改方法
def __setitem__(self, i, v): raise TypeError("ReadOnlyList is immutable")
def append(self, x): raise TypeError("ReadOnlyList is immutable")
def pop(self, *a): raise TypeError("ReadOnlyList is immutable")
# 在描述符 __get__ 中返回 ReadOnlyList(raw)✅ 总结与最佳实践
- ✅ 初始化友好:通过
hasattr(instance, self._attr_name)判断是否首次赋值,而非无条件拒绝__set__; - ✅ 命名隔离:始终用
f"_{name}"存储真实值,杜绝 descriptor 与实例属性名冲突; - ✅ 明确语义:
ReadOnly控制的是“属性是否可重新绑定”,不是“所指对象是否可变”; - ⚠️ 可变对象防护需额外设计:若需冻结内容,请在
__get__中返回不可变代理或包装器; - ? 调试提示:可通过
vars(instance)查看实际存储的私有属性,验证描述符是否按预期工作。
遵循以上模式,你就能在大型项目中安全、统一地管理只读字段,兼顾代码简洁性与运行时安全性。


















