【问题标题】:Deterministic key serialization确定性密钥序列化
【发布时间】:2011-02-27 08:14:56
【问题描述】:

我正在编写一个持久化到磁盘的映射类。我目前只允许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

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

有什么见解吗?

【问题讨论】:

    标签: python serialization pickle


    【解决方案1】:

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

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

    【讨论】:

    • 这是非常正确的。然而,由于可变对象不能用作键,我们不需要担心字典。另一方面,冷冻套装......
    【解决方案2】:

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

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

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

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

    【讨论】:

    • 很高兴知道,感谢您回到这个问题并提供答案。
    【解决方案3】:

    “对于内置 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)中的底层序列化。

    【讨论】:

    • 这可行。为了完整起见,假设我们想在 64 位机器上将 int > 2^32 视为与 long 相同,那么我们只需在编组之前将它们转换为 long,以确保它在这些机器上具有确定性。
    【解决方案4】:

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

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

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

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

    【讨论】:

    • 理想情况下,我可以使用任何值,这是内置 dict 可接受的键,但我会对字符串、unicode、int 和元组感到满意。
    猜你喜欢
    • 2013-10-22
    • 2020-11-26
    • 2014-05-10
    • 1970-01-01
    • 2021-12-27
    • 1970-01-01
    • 2014-04-27
    • 1970-01-01
    • 2013-06-02
    相关资源
    最近更新 更多