确定性密钥序列化

时间:2010-06-03 14:05:09

标签: python serialization pickle

我正在编写一个持久存储到磁盘的映射类。我目前只允许str个密钥,但如果我可以使用更多类型的话会很好:希望可以使用任何可以清除的东西(即与内置dict相同的要求),但更合理我会接受这些类型的字符串,unicode,int和元组。

为此,我想得出一个确定性的序列化方案。

选项1 - 腌制钥匙

我首先想到的是使用pickle(或cPickle)模块来序列化密钥,但我注意到picklecPickle的输出彼此不匹配:

>>> import pickle
>>> import cPickle
>>> def dumps(x):
...     print repr(pickle.dumps(x))
...     print repr(cPickle.dumps(x))
... 
>>> dumps(1)
'I1\n.'
'I1\n.'
>>> dumps('hello')
"S'hello'\np0\n."
"S'hello'\np1\n."
>>> dumps((1, 2, 'hello'))
"(I1\nI2\nS'hello'\np0\ntp1\n."
"(I1\nI2\nS'hello'\np1\ntp2\n."

是否有pickle的任何实现/协议组合对某些类型的集合是确定性的(例如,只能将cPickle与协议0一起使用)?

选项2 - Repr和ast.literal_eval

另一个选择是使用repr转储和ast.literal_eval加载。我编写了一个函数来确定给定的密钥是否能够在这个过程中存活(它允许的类型相当保守):

def is_reprable_key(key):
    return type(key) in (int, str, unicode) or (type(key) == tuple and all(
        is_reprable_key(x) for x in key))

此方法的问题是repr本身对于我在此允许的类型是否具有确定性。我相信由于str / unicode文字的改变,这将无法在2/3版本障碍中存活。这对于2**32 - 1 < x < 2**64在32位和64位平台之间跳转的整数也不起作用。是否还有其他条件(即在同一个解释器中不同条件下字符串序列化不同)? 编辑:我只是想了解这种情况发生的情况,而不一定要克服它们。

选项3:自定义代理

另一个可能过度的选择是编写我自己的repr,这会使我知道(或怀疑可能是)问题的repr变平。我刚刚在这里写了一个例子:http://gist.github.com/423945

(如果这一切都失败了,那么我可以存储密钥的散列以及密钥和值的pickle,然后遍历具有匹配散列的行,寻找一个对预期密钥进行unpickles的行,但是确实让其他一些事情变得复杂,我宁愿不这样做。编辑: it turns out内置hash也不是跨平台的确定性。抓点。)

任何见解?

4 个答案:

答案 0 :(得分:3)

重要提示:如果在您尝试序列化的对象中嵌入了字典或集类型,则repr()不具有确定性。可以按任何顺序打印密钥。

例如,print repr({'a':1, 'b':2})可能打印为{'a':1, 'b':2}{'b':2, 'a':1},具体取决于Python如何决定管理字典中的键。

答案 1 :(得分:2)

在阅读了大部分源代码(CPython 2.6.5)以实现基本类型的repr之后,我已经得出结论(有合理的信心),这些类型的repr实际上是确定性的。但是,坦率地说,这是预期的。

我认为repr方法很容易受到marshal方法崩溃的几乎所有相同情况的影响(long s&gt; 2 ** 32永远不会32位计算机上的int,不保证不会在版本或口译员之间进行更改等。)

我暂时的解决方案是使用repr方法并编写一个全面的测试套件,以确保repr在我正在使用的各种平台上返回相同的值。

从长远来看,自定义repr功能会使所有平台/实现差异变得平淡,但对于手头的项目来说肯定是过度的。不过,我可能会在将来这样做。

答案 2 :(得分:1)

“任何值是内置字典的可接受键”是不可行的:这些值包括未定义__hash__或比较的类的任意实例,隐式使用其id进行散列和比较的目的,即使在同一个程序的运行中id也不会相同(除非这些运行在所有方面都严格相同,这是非常棘手的 - 相同的输入,相同的开始时代,完全相同的环境等等。)

对于其项目都是这些类型的字符串,unicodes,int和元组(包括嵌套元组),marshal模块可以提供帮助(在单个版本的Python中:编组代码可以并且确实在不同版本之间进行更改)。 E.g:

>>> marshal.dumps(23)
'i\x17\x00\x00\x00'
>>> marshal.dumps('23')
't\x02\x00\x00\x0023'
>>> marshal.dumps(u'23')
'u\x02\x00\x00\x0023'
>>> marshal.dumps((23,))
'(\x01\x00\x00\x00i\x17\x00\x00\x00'

这是Python 2; Python 3将是类似的(除了这些字节字符串的所有表示都有一个前导b,但这是一个美化问题,当然u'23'变成无效语法,'23'变成了Unicode字符串)。您可以看到一般概念:前导字节表示类型,例如Unicode字符串为u,整数为i,元组为(;然后对于容器来说(作为小端整数)项目本身所遵循的项目数,并将整数序列化为小端形式。 marshal旨在跨平台移植(对于给定版本;不跨版本),因为它用作已编译字节码文件(.pyc.pyo)中的基础序列化。

答案 3 :(得分:0)

你在段落中提到了一些要求,我想你可能希望对这些有所了解。到目前为止,我收集了:

  • 你正在构建一个基本上是字典的SQLite后端。
  • 您希望允许键多于basetring类型(哪些类型)。
  • 你希望它能够在Python 2中存活 - &gt; Python 3屏障。
  • 您希望支持大于2 ** 32的大整数作为密钥。
  • 能够存储无限值(因为您不需要哈希冲突)。

那么,您是否正在尝试构建一个通用的“这可以做到这一切”的解决方案,或者只是尝试解决当前项目中的直接问题?你应该花一点时间来提出一套明确的要求。

使用哈希看起来对我来说似乎是最好的解决方案,但是你抱怨说你将拥有多个具有相同哈希值的行,这意味着你将存储足够的值来担心哈希值。

相关问题