在Python中对字典进行序列化处理

9

我可以期望相同的pickle字典的字符串表示在同一Python版本的不同机器或运行中是一致的吗?在同一台机器上的一个运行范围内呢?

例如:

# Python 2.7

import pickle
initial = pickle.dumps({'a': 1, 'b': 2})
for _ in xrange(1000**2):
    assert pickle.dumps({'a': 1, 'b': 2}) == initial

这是否取决于我的字典对象的实际结构(嵌套值等)?

更新: 问题在于,无论我的字典对象看起来如何(哪些键/值等),我都无法在一个运行范围内使上面的代码失败(Python 2.7)。


2
绝对不是这样的。你有使用字符串表示法的好理由吗?你正在使用xrange,这意味着Python 2中字典键的顺序是随意的(这使得字符串表示法无用)。 - DeepSpace
上面的代码可以工作(当然只是一次),所以真的“无用”吗?另外,我想要理解这样的行为,所以我有一个非常好的理由提出这样的问题 :) - d-d
5
你的字典的腌制表示可能会有所不同,如已指出。但是,即使在这种情况下,未腌制的字典与原始字典相比较是相等的,而这不是真正重要的吗? - jasonharper
不,我的问题是一个序列化对象的字符串表示很重要,仅此而已。 - d-d
2
如果您需要维护顺序和数据类型,为什么不使用collections.OrderedDict呢? - jpp
完全不相关,但您的别名行可以简单地写成pickle = dumps。如果您所做的只是传递相同数量的参数,则不需要使用lambda。 - GP89
5个回答

7

通常情况下你不能,原因与其他场景下无法依赖字典顺序排序相同;在此pickling并没有特殊性。一个字典的字符串表示是当前字典迭代顺序的函数,不管你如何加载它。

你自己的小测试过于有限,因为它没有对测试字典进行任何变异,并且没有使用可能导致冲突的键。你使用完全相同的Python源代码创建字典,因此这些将产生相同的输出顺序,因为字典的编辑历史记录完全相同,并且使用连续字母组成的两个单字符键不太可能引起冲突。

需要注意的是,你实际上测试的是字符串表示是否相等,你只测试它们的内容是否相同(两个在字符串表示上不同的字典仍然可以相等,因为相同的键值对,经过不同的插入顺序,可能会产生不同的字典输出顺序)。

在 cPython 3.6 之前,字典迭代顺序中最重要的因素是哈希键生成函数,在单个 Python 可执行文件的生命周期内必须保持稳定(否则会破坏所有字典),因此单进程测试永远不会看到基于不同哈希函数结果的字典顺序更改。
目前,所有的 pickle 协议修订版本都将字典的数据存储为键值对流;在加载流时,流被解码并且键值对按照磁盘上的顺序分配回字典中,因此至少从这个角度来看插入顺序是稳定的。但是,在不同的 Python 版本、机器架构和本地配置之间,哈希函数的结果绝对会有所不同。
  • PYTHONHASHSEED 环境变量 用于生成 strbytesdatetime 键的哈希值。该设置自 Python 2.6.8 和 3.2.3 开始提供,并且在 Python 3.3 中默认启用并设置为 random。因此,该设置因 Python 版本而异, 可以在本地设置为其他值。
  • 哈希函数产生一个 ssize_t 整数,这是一种平台相关的有符号整数类型,因此不同的体系结构可能会产生不同的哈希值,仅仅因为它们使用了更大或更小的 ssize_t 类型定义。

由于不同机器和不同 Python 运行环境下哈希函数的输出不同,您将看到字典的不同字符串表示。

最后,从cPython 3.6开始,dict类型的实现方式更改为一种更紧凑的格式,同时也恰好保留了插入顺序。从Python 3.7开始,语言规范已更改以使此行为成为强制性的,因此其他Python实现必须实现相同的语义。因此,在不同的Python实现或版本之间进行pickling和unpickling,即使所有其他因素相等,也可能导致不同的字典输出顺序。

2
不可以。这取决于很多因素,包括关键值、解释器状态和Python版本。如果需要一致的表示,请考虑使用具有规范形式的JSON。请注意,pickle 不旨在生成可靠的表示,它是纯机器(而非人类)可读的序列化程序。Python 版本的向后/向前兼容性是一件事,但仅适用于能够在解释器中反序列化相同对象的能力——即在一个版本中进行的转储并在另一个版本中进行的加载,保证具有相同的公共接口的相同行为。序列化的文本表示或内部内存结构都没有声称是相同的(如果我没记错的话,它从来没有声称过)。最简单的方法是在处理结构和/或种子处理方面存在显着差异的版本中转储相同的数据,同时使您的键不在缓存范围内(没有短整数或字符串),以检查这一点。
Python 3.5.6 (default, Oct 26 2018, 11:00:52) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> d = {'first_string_key': 1, 'second_key_string': 2}
>>> pickle.dump
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x11\x00\x00\x00second_key_stringq\x01K\x02X\x10\x00\x00\x00first_string_keyq\x02K\x01u.'

Python 3.6.7 (default, Oct 26 2018, 11:02:59) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> d = {'first_string_key': 1, 'second_key_string': 2}
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x10\x00\x00\x00first_string_keyq\x01K\x01X\x11\x00\x00\x00second_key_stringq\x02K\x02u.'

你的意思是指 JSON 中的排序键吗? - d-d
1
是的,在标准库序列化器中,它是 sort_keys=True,请参阅 https://docs.python.org/3/library/json.html#json.dump。 - Slam
这是个很好的解决方法,没错。但问题是 - JSON 不能处理我需要的所有数据类型,所以我在寻找另一种获取对象(列表、字典、类实例等)字符串表示的方法。 - d-d
1
@d-d JSON序列化器(和反序列化器)可以扩展以支持更多类型。当然,它永远不会完全取代pickle,但根据您的实际需求,它仍可能是一个可行的解决方案。 - bruno desthuilliers
是的,很好的观点。基本上我已经做了很多次,但这次讨论的目标是要理解Python中pickling的工作原理,而不是寻找解决方法。 - d-d
1
@d-d,你的问题在这里仍然得到了回答,答案是“不行”!因为它取决于很多事情。Pickling只是对象的一个格式良好的内部表示转储,您不能假定哈希集中项目的排序在每台机器上都以相同的顺序进行转储。它是机器相关的,问题不是pickle,而是像dict和set这样的某些类型的Python内部实现。例如,当在不同的机器上执行每个str调用时,您不能假定str({'a': 1, 'b': 2}) == str({'a': 1, 'b': 2})。 - Pykler

2

Python2中的字典是无序的,其顺序取决于键的哈希值,正如Martijn Pieters在这个很好的答案中所解释的那样。我认为你不能在这里使用字典,但你可以使用一个OrderedDict(需要Python 2.7或更高版本),它维护键的顺序。例如:

from collections import OrderedDict

data = [('b', 0), ('a', 0)]
d = dict(data)
od = OrderedDict(data)

print(d)
print(od)

#{'a': 0, 'b': 0}
#OrderedDict([('b', 0), ('a', 0)])

您可以像pickle字典一样pickle一个OrderedDict,但是顺序将被保留,并且在pickle相同对象时生成的字符串将是相同的。

from collections import OrderedDict
import pickle

data = [('a', 1), ('b', 2)]
od = OrderedDict(data)
s = pickle.dumps(od)
print(s)

请注意,不应在OrderedDict的构造函数中传递字典,因为键已经被放置。如果您有一个字典,您应该先将其转换为具有所需顺序的元组。OrderedDict是dict的子类,并具有所有dict方法,因此您可以创建一个空对象并分配新键。
您的测试未失败,因为您正在使用相同的Python版本和相同的条件-字典的顺序不会在循环迭代之间随机更改。但是,我们可以演示当我们改变字典中键的顺序时,您的代码如何无法产生不同的字符串。
import pickle

initial = pickle.dumps({'a': 1, 'b': 2})
assert pickle.dumps({'b': 2, 'a': 1}) != initial

当我们将键“b”放在首位时,生成的字符串应该与将键“a”放在首位时不同(在Python >= 3.6中会有所不同),但在Python2中它们是相同的,因为键“a”在键“b”之前。

回答你的主要问题,Python2字典是无序的,但使用相同的代码和Python版本时,字典可能具有相同的顺序。但是,该顺序可能与您在字典中放置项目的顺序不同。如果顺序很重要,最好使用OrderedDict或更新Python版本。


嗨@t.m.adam,希望你一切都好。如果您能提供任何解决方案,我会非常高兴您查看此帖子。谢谢。 - robots.txt

1
作为Python中令人沮丧的许多事情之一,答案是“有点”。直接从文档中得知,
“pickle序列化格式保证在Python版本之间向后兼容。”
这可能与您所询问的略微不同。如果现在是有效的pickled字典,它将始终是有效的pickled字典,并且始终会反序列化为正确的字典。这留下了一些您可能期望但不必保持的属性:
  • 即使在同一Python实例的同一平台上,对于相同对象,Pickling也不必是确定性的。同一个字典可能有无限多个可能的pickled表示(尽管我们不希望格式变得足够低效以支持任意大的额外填充)。正如其他答案所指出的那样,字典没有定义的排序顺序,这可以给出至少n!个字符串表示具有n个元素的字典。
  • 进一步说,甚至在单个Python实例中,pickle也不能保证一致性。实际上,这些更改目前并没有发生,但这种行为不能保证在未来的Python版本中保持不变。
  • 将来的Python版本不需要以与当前版本兼容的方式序列化字典。我们唯一的承诺是它们将能够正确地反序列化我们的字典。目前,在所有Pickle格式中都支持字典,但这不一定会永远保持不变(尽管我不认为它会改变)。

仅仅因为 Pickling 具有向后兼容性,并不意味着在加载 pickle 文件时会产生相同的字典顺序。 - Martijn Pieters
@MartijnPieters 我认为我们达成了一致。我应该重新组织/重新格式化我的答案以使其更清晰吗? - Hans Musgrave

0

如果您不修改字典,它的字符串表示在程序运行期间不会改变,并且其.keys方法将以相同的顺序返回键。但是,在Python 3.6之前,顺序可能会在每次运行时发生变化。

此外,具有相同键值对的两个不同字典对象不能保证使用相同的顺序(在Python 3.6之前)。


顺便说一下,使用自己的变量遮蔽模块名称不是一个好主意,就像你在那个lambda中所做的那样。这会使代码难以阅读,并且如果您忘记了已经遮蔽了该模块并尝试在程序后面访问其他名称,则会导致混淆的错误消息。


我相信,在某个时刻(3.5?)关键字的顺序甚至被_强制_在每次运行时都不同。 - Slam
1
@d-d pickle 不关心字符串表示,它使用 .items。但是你的代码不应该依赖于 pickle 维护键顺序(在 Python 3.6 之前)。 - PM 2Ring
1
@Slam 具体行为取决于PYTHONHASHSEED环境变量的设置。我记得从3.2到3.5,默认情况下使用随机哈希种子,以防止在服务器上使用字典时发生DOS攻击。 - PM 2Ring
@d-d 哦,好的。我考虑过这种可能性;通常我使用稍微更紧凑的二进制pickle协议。 - PM 2Ring
同样,在 Python 3.6 之前,具有相同键值对的两个不同字典对象不能保证使用相同的顺序。这在 Python 3.6 之后是否仍然成立?如果我理解正确,这只是说 d1 == d2 不能保证内部排序一致。 - jpp
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接