5

我正在编写一个持久化到磁盘的映射类。我目前只允许str键,但如果我可以使用更多类型会很好:希望达到任何可散列的(即与 builtin 相同的要求dict),但更合理的是我会接受字符串、unicode、int 和这些类型的元组。

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

选项 1 - 酸洗密钥

我的第一个想法是使用pickle(或cPickle)模块来序列化密钥,但我注意到输出pickle相互cPickle不匹配:

>>> 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

(如果这一切都失败了,那么我可以将键的散列与键和值的泡菜一起存储,然后遍历具有匹配散列的行,以寻找与预期键无关的行,但这确实很复杂其他一些事情,我宁愿不这样做。编辑: 事实证明,内置hash函数也不是跨平台的确定性。从头开始。)

有什么见解吗?

4

4 回答 4

3

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

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

于 2010-09-13T16:02:28.853 回答
2

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

我相信该repr方法容易受到该marshal方法崩溃的几乎所有相同情况的影响(longs > 2**32 永远不能int在 32 位机器上使用,不能保证不会在版本或解释器之间更改等) .

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

从长远来看,自定义 repr 功能将消除所有平台/实现差异,但对于手头的项目来说肯定是矫枉过正。不过,我将来可能会这样做。

于 2010-06-08T23:25:33.873 回答
1

“任何对于内置 dict 来说是可接受的键的值”是不可行的:这些值包括不定义__hash__或比较的类的任意实例,隐含地使用它们id进行散列和比较目的,并且ids 不会相同即使跨相同程序的运行(除非这些运行在所有方面都严格相同,这很难安排——相同的输入、相同的开始时间、绝对相同的环境等)。

对于所有这些类型的字符串、unicode、int 和元组(包括嵌套元组),marshal模块可以提供帮助(在单个 Python 版本中:编组代码可以并且确实会跨版本更改)。例如:

>>> 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 字符串)。可以看出大意:前导字节代表类型,比如u对于Unicode字符串,i对于整数,(对于元组;那么对于容器来说(作为一个小端整数),项目的数量后面跟着项目本身,并且整数被序列化为一个小端形式。 marshal被设计为可跨平台移植(对于给定版本;而不是跨版本),因为它用作已编译字节码文件(.pyc.pyo)中的底层序列化。

于 2010-06-03T20:37:05.583 回答
0

您在段落中提到了一些要求,我认为您可能希望对这些要求更清楚一点。到目前为止,我收集到:

  • 您正在构建一个基本上是字典的 SQLite 后端。
  • 您希望允许键超过基本字符串类型(哪些类型)。
  • 您希望它能够在 Python 2 -> Python 3 障碍中幸存下来。
  • 您希望支持 2**32 以上的大整数作为键。
  • 存储无限值的能力(因为您不希望哈希冲突)。

那么,您是在尝试构建一个通用的“这可以解决所有问题”的解决方案,还是只是尝试解决一个紧迫的问题以在当前项目中继续进行?您应该多花一点时间来提出一套明确的要求。

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

于 2010-06-03T15:22:09.217 回答