ngb*_*ngb 12 python getter properties shallow-copy
我可以使用Python的属性系统制作只读列表吗?
我创建了一个列表作为成员的Python类.在内部,我希望每次修改列表时都要做一些事情.如果这是C++,我会创建getter和setter,这样我可以在调用setter时进行簿记,并且我会让getter返回一个const引用,这样如果我尝试做,编译器就会对我大喊大叫通过getter修改列表.在Python中,我们有属性系统,因此不再需要为每个数据成员编写vanilla getter和setter(谢天谢地).但是,请考虑以下脚本:
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()
Run Code Online (Sandbox Code Playgroud)
输出:
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]
Run Code Online (Sandbox Code Playgroud)
这说明constPython中缺少概念允许我使用该append()方法修改我的列表,尽管事实上我已经将它作为属性.这可以绕过我的簿记机制(_myList),或者它可以用来修改一个人可能想要只读的列表(_readOnlyList).
一种解决方法是在getter方法中返回列表的深层副本(即return self._myList[:]).如果列表很大或者复制是在内部循环中完成的话,这可能意味着需要进行大量的额外复制.(但过早的优化是所有邪恶的根源.无论如何.)此外,虽然深层复制会阻止簿记机制被绕过,但如果有人打电话.myList.append(),他们的更改将被默默地丢弃,这可能会产生一些痛苦的调试.如果引发异常会很好,所以他们知道他们正在反对这个类的设计.
解决这个最后一个问题的方法是不使用属性系统,并制作"普通"的getter和setter方法:
def myList(self):
# No property decorator.
return self._myList[:]
def setMyList(self, myList):
print('Insert bookkeeping here')
self._myList = myList
Run Code Online (Sandbox Code Playgroud)
如果用户试图调用append(),它看起来就像foo.myList().append(8),并且那些额外的括号会让他们知道他们可能正在获取副本,而不是对内部列表数据的引用.关于这一点的负面影响是,编写这样的getter和setter是一种非Pythonic,如果该类有其他列表成员,我将不得不为那些(eww)编写getter和setter,或者创建接口不一致.(我认为一个稍微不一致的界面可能是所有邪恶中最不重要的.)
我还缺少另一种解决方案吗?可以使用Pyton的属性系统制作只读列表吗?
这两个主要建议似乎是使用元组作为只读列表或子类列表.我喜欢这两种方法.从getter返回一个元组,或者首先使用元组,可以防止使用+ =运算符,这可以是一个有用的运算符,并通过调用setter来触发簿记机制.但是,返回元组是一个单行更改,如果您想要进行防御性编程但是判断在脚本中添加其他类可能会不必要地复杂,这很好.(有时可能是极简主义并假设你不需要它.)
以下是该脚本的更新版本,其中说明了这两种方法,适用于通过Google发现此问题的任何人.
import collections
def main():
foo = Foo()
print('foo.myList:', foo.myList)
try:
foo.myList.append(4)
except RuntimeError:
print('Appending prevented.')
# Note that this triggers the bookkeeping, as we would like.
foo.myList += [3.14]
print('foo.myList:', foo.myList)
try:
foo.readOnlySequence.append(8)
except AttributeError:
print('Appending prevented.')
print('foo.readOnlySequence:', foo.readOnlySequence)
class UnappendableList(collections.UserList):
def __init__(self, *args, **kwargs):
data = kwargs.pop('data')
super().__init__(self, *args, **kwargs)
self.data = data
def append(self, item):
raise RuntimeError('No appending allowed.')
class Foo:
def __init__(self):
self._myList = [1, 2, 3]
self._readOnlySequence = [5, 6, 7]
@property
def myList(self):
return UnappendableList(data=self._myList)
@myList.setter
def myList(self, rhs):
print('Insert bookkeeping here')
self._myList = rhs
@property
def readOnlySequence(self):
# or just use a tuple in the first place
return tuple(self._readOnlySequence)
if __name__ == '__main__':
main()
Run Code Online (Sandbox Code Playgroud)
输出:
foo.myList: [1, 2, 3]
Appending prevented.
Insert bookkeeping here
foo.myList: [1, 2, 3, 3.14]
Appending prevented.
foo.readOnlySequence: (5, 6, 7)
Run Code Online (Sandbox Code Playgroud)
感谢大家.
你可以让方法返回原始列表的包装器 - collections.Sequence可能有助于编写它.或者,您可以返回tuple- 将列表复制到元组中的开销通常可以忽略不计.
但最终,如果用户想要更改基础列表,他们可以并且您无法阻止它们.(毕竟,self._myList如果他们想要,他们可以直接访问).
我认为做这样的事情的pythonic方法是记录他们不应该更改列表,如果这样做,那么当他们的程序崩溃和烧毁时,这是他们的错.