Python 中的酸洗字典
Pickling dict in Python
对于同一 Python 版本,我能否期望同一 pickled dict 的字符串表示在不同 machines/runs 之间保持一致?
在同一台机器上运行一个范围内?
例如
# 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) 的范围内失败,无论我的字典对象看起来如何(keys/values 等)
不,你不能。这取决于很多事情,包括键值、解释器状态和 python 版本。
如果您需要一致的表示,请考虑使用具有规范形式的 JSON。
编辑
我不太清楚为什么人们会在没有任何评论的情况下对此投反对票,但我会澄清一下。
pickle
并不意味着产生可靠的表示,它是纯机器(而非人类)可读的序列化程序。
Python 版本 backward/forward 兼容性是一回事,但它仅适用于反序列化相同对象的能力 inside 解释器——即当你转储到一个版本并在另一个中加载,它保证具有相同 public 接口的相同行为。序列化文本表示或内部存储器结构都没有声称相同(和 IIRC,它从来没有)。
检查这一点的最简单方法是在结构处理and/or 种子处理方面存在显着差异的版本中转储相同的数据,同时将密钥保持在缓存范围之外(没有短整数或字符串):
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.'
如果你不修改字典,它的字符串表示在给定的 运行 程序期间不会改变,它的 .keys
方法将 return 中的键同样的顺序。但是,顺序 可以 从 运行 运行 更改(在 Python 3.6 之前)。
此外,不能保证具有相同键值对的两个不同的字典对象使用相同的顺序(pre Python 3.6)。
顺便说一句,用您自己的变量隐藏模块名称不是一个好主意,就像您对 lambda 所做的那样。它使代码更难阅读,如果您忘记隐藏模块并在程序稍后尝试从中访问其他名称,则会导致错误消息令人困惑。
Python2字典是无序的;顺序取决于键的哈希值,如这个伟大的 answer by Martijn Pieters. I don't think you can use a dict here, but you could use an 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
当我们把 key 'b' 放在第一位时,结果字符串应该不同(在 Python >= 3.6 中会有所不同),但在 Python2 中是相同的,因为 key 'a' 放在键 'b' 之前。
为了回答您的主要问题,Python2 词典是无序的,但是当使用相同的代码和 Python 版本时,词典很可能具有相同的顺序。但是,该顺序可能与您在字典中放置项目的顺序不同。如果订单很重要,最好使用 OrderedDict 或更新您的 Python 版本。
与 Python 中令人沮丧的大量事物一样,答案是 "sort of"。直接来自文档,
The pickle serialization format is guaranteed to be backwards compatible across Python releases.
这可能与您的要求有细微的不同。如果它现在是一个有效的 pickled 字典,它将永远是一个有效的 pickled 字典,并且它总是反序列化为正确的字典。这留下了一些您可能期望但不必保留的属性:
- Pickling 不必是确定性的,即使对于同一平台上同一 Python 实例中的同一对象也是如此。同一个字典可以有无限多种可能的 pickled 表示(并不是说我们期望这种格式效率低到足以支持任意大程度的额外填充)。正如其他答案所指出的那样,字典没有定义的排序顺序,这至少可以给出 n!具有 n 个元素的字典的字符串表示形式。
- 在最后一点上更进一步,即使在单个 Python 实例中,也不能保证 pickle 是一致的。实际上,这些更改目前不会发生,但不能保证这种行为会保留在 Python.
的未来版本中
- Python 的未来版本不需要以与当前版本兼容的方式序列化字典。我们唯一的承诺是他们将能够正确反序列化我们的字典。目前,所有 Pickle 格式都支持相同的词典,但不必永远保持这种情况(我怀疑它不会改变)。
出于同样的原因,在一般情况下你不能这样做you can't rely on the dictionary order in other scenarios; 酸洗在这里并不特别。字典的字符串表示是当前字典迭代顺序的函数,无论您如何加载它。
你自己的小测试太有限了,因为它没有对测试字典做任何改变,也没有使用会导致冲突的键。您使用完全相同的 Python 源代码创建词典,因此它们将产生相同的输出顺序,因为词典的编辑历史完全相同,并且两个单字符键使用 ASCII 字符集中的连续字母不太可能引起碰撞。
并不是说你实际测试字符串表示是否相等,你只测试它们的内容是否相同(两个字符串表示不同的字典仍然可以相等,因为相同的键-值对,按照不同的插入顺序,可以产生不同的字典输出顺序)。
接下来,在 cPython 3.6 之前的字典迭代顺序中最重要的因素是哈希键生成函数,它必须在单个 Python 可执行生命周期内保持稳定(否则你d break all dictionaries),因此单进程测试永远不会看到字典顺序根据不同的哈希函数结果而变化。
目前,所有 pickling 协议修订版都将字典的数据存储为键值对流;在加载时,流被解码,键值对按磁盘顺序分配回字典,因此从这个角度来看,插入顺序至少是稳定的。 但是在不同的Python版本、机器架构和本地配置之间,哈希函数的结果绝对会有所不同:
PYTHONHASHSEED
environment variable 用于为 str
、bytes
和 datetime
键生成哈希值。该设置从 Python 2.6.8 和 3.2.3 开始可用,并且从 Python 3.3 开始启用并默认设置为 random
。所以设置从Python版本到Python版本不一样,和可以在本地设置不同的东西。
- 散列函数产生一个
ssize_t
整数,一种依赖于平台的有符号整数类型,因此不同的体系结构可以产生不同的散列,只是因为它们使用更大或更小的 ssize_t
类型定义。
机器与机器之间以及 Python 运行 Python 运行 的哈希函数输出不同,您 将 看到字典的不同字符串表示形式。
最后,从 cPython 3.6 开始,dict
类型的实现更改为更紧凑的格式,发生 以保留插入命令。从 Python 3.7 开始,语言规范已更改为强制执行此行为,因此其他 Python 实现必须实现相同的语义。因此,不同 Python 实现或早于 Python 3.7 的版本之间的 pickling 和 unpickling 也会导致不同的字典输出顺序,即使所有其他因素都相同。
对于同一 Python 版本,我能否期望同一 pickled dict 的字符串表示在不同 machines/runs 之间保持一致? 在同一台机器上运行一个范围内?
例如
# 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) 的范围内失败,无论我的字典对象看起来如何(keys/values 等)
不,你不能。这取决于很多事情,包括键值、解释器状态和 python 版本。
如果您需要一致的表示,请考虑使用具有规范形式的 JSON。
编辑
我不太清楚为什么人们会在没有任何评论的情况下对此投反对票,但我会澄清一下。
pickle
并不意味着产生可靠的表示,它是纯机器(而非人类)可读的序列化程序。
Python 版本 backward/forward 兼容性是一回事,但它仅适用于反序列化相同对象的能力 inside 解释器——即当你转储到一个版本并在另一个中加载,它保证具有相同 public 接口的相同行为。序列化文本表示或内部存储器结构都没有声称相同(和 IIRC,它从来没有)。
检查这一点的最简单方法是在结构处理and/or 种子处理方面存在显着差异的版本中转储相同的数据,同时将密钥保持在缓存范围之外(没有短整数或字符串):
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.'
如果你不修改字典,它的字符串表示在给定的 运行 程序期间不会改变,它的 .keys
方法将 return 中的键同样的顺序。但是,顺序 可以 从 运行 运行 更改(在 Python 3.6 之前)。
此外,不能保证具有相同键值对的两个不同的字典对象使用相同的顺序(pre Python 3.6)。
顺便说一句,用您自己的变量隐藏模块名称不是一个好主意,就像您对 lambda 所做的那样。它使代码更难阅读,如果您忘记隐藏模块并在程序稍后尝试从中访问其他名称,则会导致错误消息令人困惑。
Python2字典是无序的;顺序取决于键的哈希值,如这个伟大的 answer by Martijn Pieters. I don't think you can use a dict here, but you could use an 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
当我们把 key 'b' 放在第一位时,结果字符串应该不同(在 Python >= 3.6 中会有所不同),但在 Python2 中是相同的,因为 key 'a' 放在键 'b' 之前。
为了回答您的主要问题,Python2 词典是无序的,但是当使用相同的代码和 Python 版本时,词典很可能具有相同的顺序。但是,该顺序可能与您在字典中放置项目的顺序不同。如果订单很重要,最好使用 OrderedDict 或更新您的 Python 版本。
与 Python 中令人沮丧的大量事物一样,答案是 "sort of"。直接来自文档,
The pickle serialization format is guaranteed to be backwards compatible across Python releases.
这可能与您的要求有细微的不同。如果它现在是一个有效的 pickled 字典,它将永远是一个有效的 pickled 字典,并且它总是反序列化为正确的字典。这留下了一些您可能期望但不必保留的属性:
- Pickling 不必是确定性的,即使对于同一平台上同一 Python 实例中的同一对象也是如此。同一个字典可以有无限多种可能的 pickled 表示(并不是说我们期望这种格式效率低到足以支持任意大程度的额外填充)。正如其他答案所指出的那样,字典没有定义的排序顺序,这至少可以给出 n!具有 n 个元素的字典的字符串表示形式。
- 在最后一点上更进一步,即使在单个 Python 实例中,也不能保证 pickle 是一致的。实际上,这些更改目前不会发生,但不能保证这种行为会保留在 Python. 的未来版本中
- Python 的未来版本不需要以与当前版本兼容的方式序列化字典。我们唯一的承诺是他们将能够正确反序列化我们的字典。目前,所有 Pickle 格式都支持相同的词典,但不必永远保持这种情况(我怀疑它不会改变)。
出于同样的原因,在一般情况下你不能这样做you can't rely on the dictionary order in other scenarios; 酸洗在这里并不特别。字典的字符串表示是当前字典迭代顺序的函数,无论您如何加载它。
你自己的小测试太有限了,因为它没有对测试字典做任何改变,也没有使用会导致冲突的键。您使用完全相同的 Python 源代码创建词典,因此它们将产生相同的输出顺序,因为词典的编辑历史完全相同,并且两个单字符键使用 ASCII 字符集中的连续字母不太可能引起碰撞。
并不是说你实际测试字符串表示是否相等,你只测试它们的内容是否相同(两个字符串表示不同的字典仍然可以相等,因为相同的键-值对,按照不同的插入顺序,可以产生不同的字典输出顺序)。
接下来,在 cPython 3.6 之前的字典迭代顺序中最重要的因素是哈希键生成函数,它必须在单个 Python 可执行生命周期内保持稳定(否则你d break all dictionaries),因此单进程测试永远不会看到字典顺序根据不同的哈希函数结果而变化。
目前,所有 pickling 协议修订版都将字典的数据存储为键值对流;在加载时,流被解码,键值对按磁盘顺序分配回字典,因此从这个角度来看,插入顺序至少是稳定的。 但是在不同的Python版本、机器架构和本地配置之间,哈希函数的结果绝对会有所不同:
PYTHONHASHSEED
environment variable 用于为str
、bytes
和datetime
键生成哈希值。该设置从 Python 2.6.8 和 3.2.3 开始可用,并且从 Python 3.3 开始启用并默认设置为random
。所以设置从Python版本到Python版本不一样,和可以在本地设置不同的东西。- 散列函数产生一个
ssize_t
整数,一种依赖于平台的有符号整数类型,因此不同的体系结构可以产生不同的散列,只是因为它们使用更大或更小的ssize_t
类型定义。
机器与机器之间以及 Python 运行 Python 运行 的哈希函数输出不同,您 将 看到字典的不同字符串表示形式。
最后,从 cPython 3.6 开始,dict
类型的实现更改为更紧凑的格式,发生 以保留插入命令。从 Python 3.7 开始,语言规范已更改为强制执行此行为,因此其他 Python 实现必须实现相同的语义。因此,不同 Python 实现或早于 Python 3.7 的版本之间的 pickling 和 unpickling 也会导致不同的字典输出顺序,即使所有其他因素都相同。