如何实现Python只读属性通用Descriptor?含列表元素防修改
将@property只读逻辑重构为通用Descriptor并解决可变对象修改问题
一、解决Descriptor初始化报错问题
原Descriptor实现中,类内部初始化属性时会触发__set__方法抛出异常,核心原因是它没有区分外部赋值和内部对私有属性的操作。我们可以调整Descriptor的设计,让它映射到实例的私有存储属性,类内部直接操作私有属性,外部通过Descriptor访问,完全复现@property的只读逻辑。
修改后的ReadOnly Descriptor实现:
class ReadOnly: def __set_name__(self, owner, name): # 将公开属性名映射为实例的私有存储属性(例如:n -> _n) self._storage_name = '_' + name def __get__(self, instance, owner): if instance is None: return self # 从实例的私有属性中取值 return getattr(instance, self._storage_name) def __set__(self, instance, value): raise AttributeError(f"Cannot set readonly attribute '{self._storage_name[1:]}'") def __delete__(self, instance): raise AttributeError(f"Cannot delete readonly attribute '{self._storage_name[1:]}'")
重构后的类A:
class A: n = ReadOnly() top = ReadOnly() def __init__(self, n): # 直接操作私有属性,不会触发Descriptor的__set__方法 self._n = n self._top = -1 def increase(self): # 内部修改私有属性,正常执行 self._top += 1
此时外部代码尝试a.n = 10会抛出AttributeError,但类内部可以自由修改self._n和self._top,和@property的行为完全一致。
二、禁止只读属性中可变对象的修改
当只读属性指向列表、字典这类可变对象时,虽然不能重新赋值属性本身,但外部仍可修改对象内部元素。要解决这个问题,我们需要在Descriptor返回值时,将可变对象包装为不可变容器。
步骤1:实现不可变列表包装类
class ImmutableList: def __init__(self, data): self._data = list(data) # 仅实现读取相关方法 def __getitem__(self, index): return self._data[index] def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def __repr__(self): return repr(self._data) # 重写所有修改方法,抛出异常阻止修改 def __setitem__(self, index, value): raise TypeError("Cannot modify immutable list") def append(self, value): raise TypeError("Cannot modify immutable list") def extend(self, iterable): raise TypeError("Cannot modify immutable list") def insert(self, index, value): raise TypeError("Cannot modify immutable list") def remove(self, value): raise TypeError("Cannot modify immutable list") def pop(self, index=-1): raise TypeError("Cannot modify immutable list") def clear(self): raise TypeError("Cannot modify immutable list") def sort(self, key=None, reverse=False): raise TypeError("Cannot modify immutable list") def reverse(self): raise TypeError("Cannot modify immutable list")
步骤2:升级ReadOnly Descriptor支持转换函数
给Descriptor添加可选的converter参数,用于在返回值时将可变对象转换为不可变包装:
class ReadOnly: def __init__(self, converter=None): self._converter = converter def __set_name__(self, owner, name): self._storage_name = '_' + name def __get__(self, instance, owner): if instance is None: return self value = getattr(instance, self._storage_name) # 如果有转换函数,返回转换后的不可变对象 if self._converter is not None: return self._converter(value) return value def __set__(self, instance, value): raise AttributeError(f"Cannot set readonly attribute '{self._storage_name[1:]}'") def __delete__(self, instance): raise AttributeError(f"Cannot delete readonly attribute '{self._storage_name[1:]}'")
重构后的Stack类:
class Stack: # 对S属性使用ImmutableList转换 S = ReadOnly(converter=ImmutableList) n = ReadOnly() top = ReadOnly() def __init__(self, n): self._S = [None] * n self._n = n self._top = -1 def push(self, value): if self._top >= self._n - 1: raise IndexError("Stack overflow") self._top += 1 # 内部直接修改私有列表,不受影响 self._S[self._top] = value
现在外部代码尝试s.S[3] = 3或s.S.append(5)都会抛出TypeError,而类内部可以正常修改self._S,完全实现了属性的只读(包括内部元素不可修改)。
内容的提问来源于stack exchange,提问作者Yehui He




