我正在做一个聚合配置文件解析工具,希望它能支持.json
,.yaml
和.toml
文件。所以,我做了下面的测试:
example.json
配置文件如下:
{
"DEFAULT":
{
"ServerAliveInterval": 45,
"Compression": true,
"CompressionLevel": 9,
"ForwardX11": true
},
"bitbucket.org":
{
"User": "hg"
},
"topsecret.server.com":
{
"Port": 50022,
"ForwardX11": false
},
"special":
{
"path":"C:\Users",
"escaped1":"nt",
"escaped2":"\n\t"
}
}
example.yaml
配置文件如下:
DEFAULT:
ServerAliveInterval: 45
Compression: yes
CompressionLevel: 9
ForwardX11: yes
bitbucket.org:
User: hg
topsecret.server.com:
Port: 50022
ForwardX11: no
special:
path: C:Users
escaped1: "nt"
escaped2: nt
,example.toml
配置文件如下:
[DEFAULT]
ServerAliveInterval = 45
Compression = true
CompressionLevel = 9
ForwardX11 = true
['bitbucket.org']
User = 'hg'
['topsecret.server.com']
Port = 50022
ForwardX11 = false
[special]
path = 'C:Users'
escaped1 = "nt"
escaped2 = 'nt'
那么,输出的测试代码如下:
import pickle,json,yaml
# TOML, see https://github.com/hukkin/tomli
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
path = "example.json"
with open(path) as file:
config1 = json.load(file)
assert isinstance(config1,dict)
pickled1 = pickle.dumps(config1)
path = "example.yaml"
with open(path, 'r', encoding='utf-8') as file:
config2 = yaml.safe_load(file)
assert isinstance(config2,dict)
pickled2 = pickle.dumps(config2)
path = "example.toml"
with open(path, 'rb') as file:
config3 = tomllib.load(file)
assert isinstance(config3,dict)
pickled3 = pickle.dumps(config3)
print(config1==config2) # True
print(config2==config3) # True
print(pickled1==pickled2) # False
print(pickled2==pickled3) # True
那么,我的问题是,既然解析的对象都是字典,并且这些字典彼此相等,为什么它们的pickled
代码不相同,也就是说,为什么从json
解析的字典的pickled
代码与其他两个不同?
提前感谢。
差异是由于:
-
json
模块对具有相同值的对象属性执行记忆(它没有对它们进行实习,但是扫描器对象包含一个memo
dict
,它使用它在单个解析运行中删除相同的属性字符串),,而yaml
不(它只是在每次看到相同的数据时生成一个新的str
),和 -
pickle
忠实地复制确切的数据的结构,将对相同对象的后续引用替换为对第一次看到的反向引用(除其他原因外,这使得可以转储递归数据结构,例如lst = []
,lst.append(lst)
,而无需无限递归,并在解pickle时忠实地再现它们)
问题#1在相等性测试中不可见(str
与相同的数据比较相等,而不仅仅是内存中完全相同的对象)。但是当pickle
第一次看到"ForwardX11"
时,它插入对象的pickle形式,并发出一个pickle操作码,为该对象分配一个数字。如果再次看到那个精确的对象(相同的内存地址,而不仅仅是相同的值),它不会重新序列化它,而是发出一个更简单的操作码,它只是说"去找到与上次的数字相关的对象,并把它放在这里"。如果它是一个不同的对象,即使它有相同的值,它是新的,并被单独序列化(并分配另一个数字,以防再次看到新对象)。
简化您的代码来演示这个问题,您可以检查生成的pickle输出,看看这是如何发生的:
s = r'''{
"DEFAULT":
{
"ForwardX11": true
},
"FOO":
{
"ForwardX11": false
}
}'''
s2 = r'''DEFAULT:
ForwardX11: yes
FOO:
ForwardX11: no
'''
import io, json, yaml, pickle, pickletools
d1 = json.load(io.StringIO(s))
d2 = yaml.safe_load(io.StringIO(s2))
pickletools.dis(pickle.dumps(d1))
pickletools.dis(pickle.dumps(d2))
上网试试!
对于json
解析的输入,该代码的输出是(#
注释内联指出重要的事情),至少在Python 3.7上(默认的pickle协议和精确的pickle格式可以在不同的版本中更改):
0: x80 PROTO 3
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: X BINUNICODE 'DEFAULT'
18: q BINPUT 1
20: } EMPTY_DICT
21: q BINPUT 2
23: X BINUNICODE 'ForwardX11' # Serializes 'ForwardX11'
38: q BINPUT 3 # Assigns the serialized form the ID of 3
40: x88 NEWTRUE
41: s SETITEM
42: X BINUNICODE 'FOO'
50: q BINPUT 4
52: } EMPTY_DICT
53: q BINPUT 5
55: h BINGET 3 # Looks up whatever object was assigned the ID of 3
57: x89 NEWFALSE
58: s SETITEM
59: u SETITEMS (MARK at 5)
60: . STOP
highest protocol among opcodes = 2
而yaml
加载数据的输出为:
0: x80 PROTO 3
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: X BINUNICODE 'DEFAULT'
18: q BINPUT 1
20: } EMPTY_DICT
21: q BINPUT 2
23: X BINUNICODE 'ForwardX11' # Serializes as before
38: q BINPUT 3 # and assigns code 3 as before
40: x88 NEWTRUE
41: s SETITEM
42: X BINUNICODE 'FOO'
50: q BINPUT 4
52: } EMPTY_DICT
53: q BINPUT 5
55: X BINUNICODE 'ForwardX11' # Doesn't see this 'ForwardX11' as being the exact same object, so reserializes
70: q BINPUT 6 # and marks again, in case this copy is seen again
72: x89 NEWFALSE
73: s SETITEM
74: u SETITEMS (MARK at 5)
75: . STOP
highest protocol among opcodes = 2
print
将每个字符串的id
替换为类似的信息,例如,将pickletools
行替换为:
for k in d1['DEFAULT']:
print(id(k))
for k in d1['FOO']:
print(id(k))
for k in d2['DEFAULT']:
print(id(k))
for k in d2['FOO']:
print(id(k))
将在d1
中显示一致的'ForwardX11'
s,但在d2
中显示不同的CC_28 s;生成了一个示例运行(添加了内联注释):
140067902240944 # First from d1
140067902240944 # Second from d1 is *same* object
140067900619760 # First from d2
140067900617712 # Second from d2 is unrelated object (same value, but stored separately)
虽然我没有检查toml
是否以相同的方式表现,鉴于它与yaml
腌制相同,它显然没有试图重复字符串;json
是唯一奇怪的。它如此在意你,这主意倒不坏;JSONdict
的键在逻辑上等同于对象的属性,对于巨大的输入(例如,一个数组中有10M个对象具有相同的几个键),它可能通过删除在最终解析输出上节省大量的内存(例如,在CPython 3.11 x86-64构建中,用单个副本替换10M个"ForwardX11"
副本将减少590 MB的字符串数据,仅为59字节)。
作为旁注:这"dict
s是相等的,咸菜不是;也可能出现问题:
- 当两个
dict
s使用相同的键和值构造时,但是键的插入顺序不同(现代Python使用插入顺序的dict
s;它们之间的比较忽略了顺序,但pickle
将以它们自然迭代的顺序序列化它们)。 - 当存在比较相等但类型不同的对象时(例如
set
vs.frozenset
,int
vs.float
);pickle
会将它们分开处理,但相等性测试不会看到差异。
这两个都不是这里的问题(json
和yaml
似乎都以输入中看到的相同顺序构造,并且它们将int
s解析为int
s),但是您的相等性测试完全有可能返回True
,而pickle形式是不相等的,即使所有涉及的对象都是唯一的。