Python Cookbook-7.7 通过 shelve 修改对象
任务
你正在使用标准库模块shelve。你用shelve处理过的一些值是易变的对象(mutableobjects),而且你需要修改这些对象。
解决方案
shelve 模块提供了一种持久的字典——在强大的关系型数据库和简洁的 marshal、pickledbm 以及类似的文件格式之间,它有着重要的地位。然而,在使用shelve的时候有一些很典型的陷阱需要注意。先看看下面的交互式 Python 会话:
>>> import shelve
>>> #创建一个简单的例子shelf
>>> she = shelve.open('try.she', 'c')
>>>> for c in 'spam':she[e] = {c:23}
...
>>> for c in she.keys():print c, she[c]
...
p{'p':23}
s{'s':23}
a{'a':23}
m{'m':23}
>>> she.close()
这样我们就创建了 shelve 文件,向其中添加了数据,之后又关闭此文件,到现在为止一切正常。现在我们需要再次打开该文件并处理其中的数据:
>>> she = shelve.open('try.she', 'c')
>>> she ['p']
{'p':23}
>>> she['p']['p'] = 42
>>> she['p']
{'p':23}
这是怎么回事?我们明明把这个值设为 42,怎么 shelve 好像根本不理会我们的设定?问题的关键在于我们处理的其实只是 shelve 给我们的一个临时对象而已,而不是“真家伙”。当我们以默认选项打开 shelve 时,就像上面做法一样,它并不会跟踪对临时对象的修改。一个可行的方案是我们给临时对象绑定一个名字,然后完成我们的修改,之后再把改过的对象赋值给shelve 的子项:
>>> a = she['p']
>>> a['p'] = 42
>>> she['p'] = a
>>> she ['p']
{'P':42}
>>> she.close()
我们可以验证这个修改是否持久:
>>> she = shelve.open('try.she','c')
>>> for c in she.keys():print c,she[c]
...
p{'p':42}
s{'s':23}
a{'a':23}
m{'m':23}
一个更简单的方法是将 writeback选项设置为True,然后打开 shelve 对象:
>>> she = shelve.open('try.she','c',writeback=True)
打开 writeback 选项后,shelve 会跟踪所有从文件生成的对象,并在关闭之前将所有项回写到文件,这是因为在这个过程中它们可能被修改过。虽然代码简化了不少,但代价是高昂的,尤其是内存的消耗增加了很多。当我们需要从一个用 writeback=True 选项打开的 shelve 对象中读取很多对象时,即使我们只修改了这些对象中的少数几个,shelve仍然会把所有对象放入内存,这是因为它事先也不知道哪个对象会被修改。而前一个方法,我们主动担起责任来通知 shelve 发生的变化(通过将修改过的对象赋值回去),需要多花工夫,但是收获也不小,因为它的效率很高。
讨论
Python 标准库模块 shelve 在很多应用场合下都是很方便的工具,但它却隐藏了一个令人生厌的陷阱,虽然在 Python 的在线文档中对此有很详细的说明,但仍然很容易被忽略。假设你正在用 shelve 处理一些易变的对象,如字典或列表。很自然的,你可能会希望更改其中的一些对象,比如调用一些修改的方法(列表的 append、字典的 update等)或给对象的子项或属性赋予一个新值。不过,当你在做这些事的时候,变化并不会发生在 shelve 对象中。这是因为我们更改的不过是 shelve 对象通过__getitem__方法提供的一个临时对象而已,而且 shelve 对象在默认情况下并不会跟踪临时对象的变化,事实上,在把临时对象返回给我们之后,它就对临时对象完全不闻不问,不关心了。
如前面代码所示,一个方法是给临时对象绑定一个名字,通过这个名字完成对临时对象的所有操作,然后将更改过的新对象赋值给 shelve 对象的子项。当你给 shelve 对象的子项赋值时,shelve对象的__setitem__方法会被调用,它会以适当的方式更新 shelve对象,这样变化就被保存下来了。
另一个可选的方法是,可以在打开shelve 对象的时候增加 writeback=True 标志,之后 shelve 会跟踪每一个它给你的对象,并最终将它们存回磁盘。这个方法会节省一些琐碎的代码,更加省心,但是你要当心:如果你从shelve对象读取很多子项,但却只修改其中的少数几项,writeback方式的开销会很大,尤其是内存消耗。当你用writeback=True选项打开shelve 时,它会把所有它给你的子项放入内存,并在最后保存,这是因为它没有什么可靠的办法来判断你要改哪些项,而且也不知道当你关闭shelve 对象时究竟哪些项是被改过的。除非你要修改每一个读取的项(或者shelve 对象的大小和你的计算机内存总数比起来微不足道,可以忽略),否则我推荐前一种方法:给你从 shelve对象获得的需要修改的子项绑定一个名字,改完之后将每个子项赋值回 shelve 对象。