使用属性装饰器实现Python只读列表
简短版本
我可以用Python的属性系统来创建一个只读列表吗?
详细版本
我创建了一个Python类,这个类里面有一个列表。每次我想在这个列表被修改时,内部都需要做一些事情。如果这是C++,我会创建一些获取和设置的方法,这样每当调用设置方法时,我就可以处理我的记录。而获取方法会返回一个const
引用,这样如果我试图通过获取方法修改列表,编译器就会警告我。在Python中,我们有属性系统,所以不再需要为每个数据成员写普通的获取和设置方法了,这真是太好了。
但是,考虑以下脚本:
def main():
foo = Foo()
print('foo.myList:', foo.myList)
# Here, I'm modifying the list without doing any bookkeeping.
foo.myList.append(4)
print('foo.myList:', foo.myList)
# Here, I'm modifying my "read-only" list.
foo.readOnlyList.append(8)
print('foo.readOnlyList:', foo.readOnlyList)
class Foo:
def __init__(self):
self._myList = [1, 2, 3]
self._readOnlyList = [5, 6, 7]
@property
def myList(self):
return self._myList
@myList.setter
def myList(self, rhs):
print("Insert bookkeeping here")
self._myList = rhs
@property
def readOnlyList(self):
return self._readOnlyList
if __name__ == '__main__':
main()
输出:
foo.myList: [1, 2, 3]
# Note there's no "Insert bookkeeping here" message.
foo.myList: [1, 2, 3, 4]
foo.readOnlyList: [5, 6, 7, 8]
这个例子说明了Python没有const
这个概念,允许我通过append()
方法修改我的列表,尽管我已经把它设为属性。这可能会绕过我的记录机制(_myList
),或者被用来修改那些我希望是只读的列表(_readOnlyList
)。
一个解决办法是在获取方法中返回列表的深拷贝(也就是return self._myList[:]
)。如果列表很大,或者在内部循环中进行拷贝,这可能会导致很多额外的复制。(不过,过早优化是万恶之源。)此外,虽然深拷贝可以防止记录机制被绕过,但如果有人调用.myList.append()
,他们的修改会被默默丢弃,这可能会导致调试时的痛苦。如果能抛出一个异常,那就好了,这样他们就知道自己在违背类的设计。
解决这个最后问题的一个办法是不使用属性系统,而是创建“正常”的获取和设置方法:
def myList(self):
# No property decorator.
return self._myList[:]
def setMyList(self, myList):
print('Insert bookkeeping here')
self._myList = myList
如果用户尝试调用append()
,看起来会像foo.myList().append(8)
,这些额外的括号会让他们意识到他们可能得到的是一个拷贝,而不是内部列表数据的引用。这样做的负面影响是,这种写法有点不符合Python的风格,如果这个类还有其他列表成员,我就得为那些写获取和设置方法(真让人头疼),或者让接口不一致。(我觉得稍微不一致的接口可能是最小的恶。)
还有其他我没想到的解决方案吗?可以用Python的属性系统创建一个只读列表吗?
6 个回答
为什么列表比元组更好用呢?元组在很多情况下可以看作是“不可变的列表”,也就是说,它们的内容一旦创建就不能被直接修改或改变。这样一来,元组就像是只读的对象,不能随便改动。如果你用元组,就需要记住不要写可以改变这个属性的代码。
>>> class A(object):
... def __init__(self, list_data, tuple_data):
... self._list = list(list_data)
... self._tuple = tuple(tuple_data)
... @property
... def list(self):
... return self._list
... @list.setter
... def list(self, new_v):
... self._list.append(new_v)
... @property
... def tuple(self):
... return self._tuple
...
>>> Stick = A((1, 2, 3), (4, 5, 6))
>>> Stick.list
[1, 2, 3]
>>> Stick.tuple
(4, 5, 6)
>>> Stick.list = 4 ##this feels like a weird way to 'cover-up' list.append, but w/e
>>> Stick.list = "Hey"
>>> Stick.list
[1, 2, 3, 4, 'Hey']
>>> Stick.tuple = 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>
这可以通过使用 Sequence
类型提示来实现。与 list
不同,Sequence
是不可修改的:
from typing import Sequence
def foo() -> Sequence[int]:
return []
result = foo()
result.append(10)
result[0] = 10
当尝试修改一个被 Sequence
提示的列表时,mypy
和 pyright
都会报错:
$ pyright /tmp/foo.py
/tmp/foo.py:7:8 - error: Cannot access member "append" for type "Sequence[int]"
Member "append" is unknown (reportGeneralTypeIssues)
/tmp/foo.py:8:1 - error: "__setitem__" method not defined on type "Sequence[int]" (reportGeneralTypeIssues)
2 errors, 0 warnings, 0 informations
不过,Python 本身并不会在意这些提示,所以需要定期运行这些类型检查工具,或者把它们作为构建过程的一部分。
还有一个 Final
类型提示,类似于 C++ 中的 const
,它只保护存储列表引用的变量,而不是保护列表本身,所以在这里用处不大,但在其他情况下可能会有用。
提到的解决方案,比如返回一个元组或者继承列表,听起来不错,但我在想,直接继承装饰器是不是更简单呢?我不确定这是不是个傻主意:
- 使用这个
safe_property
可以防止意外发送混合的API信号(内部不可变的属性是“保护”的,不能进行任何操作,而对于可变属性,某些操作在正常的property
内置中仍然是允许的) - 优点:比到处实现自定义返回类型要简单得多——更容易理解和使用
- 缺点:需要使用不同的名称
class FrozenList(list):
def _immutable(self, *args, **kws):
raise TypeError('cannot change object - object is immutable')
pop = _immutable
remove = _immutable
append = _immutable
clear = _immutable
extend = _immutable
insert = _immutable
reverse = _immutable
class FrozenDict(dict):
def _immutable(self, *args, **kws):
raise TypeError('cannot change object - object is immutable')
__setitem__ = _immutable
__delitem__ = _immutable
pop = _immutable
popitem = _immutable
clear = _immutable
update = _immutable
setdefault = _immutable
class safe_property(property):
def __get__(self, obj, objtype=None):
candidate = super().__get__(obj, objtype)
if isinstance(candidate, dict):
return FrozenDict(candidate)
elif isinstance(candidate, list):
return FrozenList(candidate)
elif isinstance(candidate, set):
return frozenset(candidate)
else:
return candidate
class Foo:
def __init__(self):
self._internal_lst = [1]
@property
def internal_lst(self):
return self._internal_lst
@safe_property
def internal_lst_safe(self):
return self._internal_lst
if __name__ == '__main__':
foo = Foo()
foo.internal_lst.append(2)
# foo._internal_lst is now [1, 2]
foo.internal_lst_safe.append(3)
# this throws an exception
我很想听听其他人的看法,因为我还没见过其他地方实现这个。
你可以让方法返回一个包装器,包裹住你原来的列表——可以用collections.Sequence
来帮助你写这个包装器。或者,你也可以返回一个tuple
(元组)——把列表复制到元组里的开销通常是很小的。
不过,最终如果用户想要修改底层的列表,他们是可以做到的,你真的没办法阻止他们。(毕竟,他们可以直接访问self._myList
,如果他们想的话)。
我觉得在Python中,处理这种情况的好方法是明确说明他们不应该去修改这个列表,如果他们真的去改了,那程序崩溃了就是他们自己的责任了。