cPickle - 对同一对象进行序列化得到不同结果
有没有人能解释一下在 testLookups()
下面的评论是什么意思?这个评论在这个代码片段中。
我运行了这段代码,确实评论里说的是真的。不过我想搞明白为什么会这样,也就是说,为什么同一个对象在不同的引用方式下,cPickle 输出的值会不一样。
这和引用计数有关系吗?如果有的话,这难道不是某种bug吗?也就是说,被序列化和反序列化的对象会有异常高的引用计数,结果永远不会被垃圾回收吗?
2 个回答
看起来相同的对象不一定会生成相同的pickle字符串。
pickle协议就像一个虚拟机器,而pickle字符串就是这个虚拟机器的程序。对于一个特定的对象,可能会有多个pickle字符串(也就是程序)能够完全重建这个对象。
举个你提到的例子:
>>> from cPickle import dumps
>>> t = ({1: 1, 2: 4, 3: 6, 4: 8, 5: 10}, 'Hello World', (1, 2, 3, 4, 5), [1, 2, 3, 4, 5])
>>> dumps(({1: 1, 2: 4, 3: 6, 4: 8, 5: 10}, 'Hello World', (1, 2, 3, 4, 5), [1, 2, 3, 4, 5]))
"((dp1\nI1\nI1\nsI2\nI4\nsI3\nI6\nsI4\nI8\nsI5\nI10\nsS'Hello World'\np2\n(I1\nI2\nI3\nI4\nI5\ntp3\n(lp4\nI1\naI2\naI3\naI4\naI5\nat."
>>> dumps(t)
"((dp1\nI1\nI1\nsI2\nI4\nsI3\nI6\nsI4\nI8\nsI5\nI10\nsS'Hello World'\n(I1\nI2\nI3\nI4\nI5\nt(lp2\nI1\naI2\naI3\naI4\naI5\natp3\n."
这两个pickle字符串在使用p
这个操作码时有所不同。这个操作码需要一个整数作为参数,它的作用如下:
name='PUT' code='p' arg=decimalnl_short
Store the stack top into the memo. The stack is not popped.
The index of the memo location to write into is given by the newline-
terminated decimal string following. BINPUT and LONG_BINPUT are
space-optimized versions.
简单来说,这两个pickle字符串基本上是等价的。
我还没有深入研究生成的操作码差异的具体原因。这可能和被序列化对象的引用计数有关。不过很明显,这种差异不会影响重建出来的对象。
这段话在讲的是关于引用计数的内容,来自cPickle的源代码:
if (Py_REFCNT(args) > 1) {
if (!( py_ob_id = PyLong_FromVoidPtr(args)))
goto finally;
if (PyDict_GetItem(self->memo, py_ob_id)) {
if (get(self, py_ob_id) < 0)
goto finally;
res = 0;
goto finally;
}
}
在进行对象的序列化(也就是把对象转成可以存储或传输的格式)时,pickle协议需要处理多个地方引用同一个对象的情况。为了避免在反序列化(把存储的格式再转回对象)时重复创建这个对象,它使用了一个叫做“备忘录”的东西。这个备忘录基本上是一个索引和对象之间的映射关系。pickle中的PUT (p)操作码会把当前对象存储到这个备忘录字典里。
不过,如果一个对象只有一个引用,那就没有必要把它存到备忘录里,因为它只会被引用一次,没必要再找它。因此,cPickle的代码在这个时候会检查引用计数,以进行一些小优化。
所以说,是的,确实是引用计数的问题。但这并不是个大问题。反序列化后的对象会有正确的引用计数,只是当引用计数为1时,生成的pickle会稍微短一些。
现在,我不知道你在做什么,为什么会在意这个。但你真的不应该假设序列化同一个对象每次都会得到相同的结果。至少,我认为字典可能会出问题,因为字典的键的顺序是不确定的。除非你有Python的文档保证每次序列化的结果都是一样的,否则我强烈建议你不要依赖这个。