我正在开发一个聚合配置文件解析工具,希望它能支持.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":"\n\t",
      "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: "\n\t"
  escaped2: \n\t

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 = "\n\t"
escaped2 = '\n\t'

然后,带有输出的测试代码如下:

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

那么,我的问题是,既然解析的obj都是字典,而且这些字典彼此相等,为什么它们的pickled个代码不相同,即为什么从json解析的字典的pickled个代码与其他两个不同?

先谢谢你.

推荐答案

造成差异的原因是:

  1. The 101 module is performing memoizing for object attributes with the same value(不是实例化它们,而是用来在单个解析运行中消除相同属性字符串的重复数据的the scanner object contains a memo dict)、while 102 does not(每次它看到相同的数据就生成一个新的str),以及

  2. 100 faithfully reproducing the exact structure of the data it's told to dump, replacing subsequent references to the same object with a back-reference to the first time it was seen(在其他原因中,这使得有可能在没有无限递归的情况下转储递归数据 struct ,例如lst = []lst.append(lst),并在未挑选时忠实地再现它们)

问题1在相等性测试中是不可见的(str与相同的数据比较相等,而不仅仅是内存中的相同对象).但当pickle第一次看到"ForwardX11"时,它会插入对象的PICKLE形式,并发出一个为该对象分配编号的PICKLE操作码.如果再次看到exact对象(相同的内存地址,而不仅仅是相同的值),它不会重新序列化它,而是发出一个更简单的操作码,它只会说"找到与上次的数字相关联的对象,并将其也放在这里".但是,如果它是一个不同的对象,即使是具有相同值的对象,它也是新的,并被单独序列化(并分配另一个数字,以防再次看到新对象).

为了简化代码以演示该问题,您可以判断生成的PICLE输出,以了解这是如何发生的:

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

Try it online!

对于json个已解析的输入,该代码的输出是(内联#条注释以指出重要的事情),至少在Python3.7(默认的PICLE协议和确切的PICLING格式可能因版本而异)上是:

    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'显示一致的id,但为d2显示不同的值;生成了一个样例运行(添加了行内注释):

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相同,但考虑到它与yaml一样,它显然不是在试图删除字符串;json在那里是唯一奇怪的.请注意,它这样做并不是一个糟糕的 idea ;JSON dict的键在逻辑上等同于对象上的属性,对于巨大的输入(比如,一个数组中有10M个对象,具有相同数量的键),它可能会通过重复数据删除在最终解析的输出上节省大量内存(例如,在CPython3.11x86-64版本上,用单个副本替换10M个"ForwardX11"副本将把59MB的字符串数据减少到59个字节).


作为附注:这个"dict等于,泡菜不等于"的问题可能会发生:

  1. 当这两个dict是用相同的键和值构造的,但插入键的顺序不同时(现代的Python使用插入顺序的dict;它们之间的比较忽略了顺序,但pickle将以它们自然迭代的顺序序列化它们).
  2. 当存在比较相同但类型不同的对象时(例如,setfrozenset,intfloat);pickle会将它们分开处理,但相等性测试看不到区别.

这两者都不是这里的问题(jsonyaml的构造顺序似乎与输入中看到的顺序相同,并且它们将int解析为int),但是您的相等测试完全有可能返回True,而酸洗的形式是不相等的,即使涉及的所有对象都是唯一的.

Json相关问答推荐

如何使用JQ将JSON字符串替换为解析后的类似功能?

JSON:将项';S键/名称移动到属性中,并使用JQ将其转换为数组

我发现GoFr响应总是包含在数据字段中,如何返回自定义响应?

JSON API返回多个数组,需要帮助拼合数据以存储在SQL Server数据库表中

使用JQ合并JSON列表中的对象

Jolt Spec 跳过数组中的第一个元素

为什么解析的字典相等而腌制的字典不相等?

Oracle Apex - 将 JSON 对象分配给变量以返回

使用带有逗号的字段名称构建 struct

MarkLogic REST 资源 API - 仅使用一个 POST 请求修补多个文档

流导入错误:重新上传时不存在布局释放 UUID

传统编程语言等价于动态 SQL

解析 JSON API 响应

在 Apache Spark 中读取多行 JSON

使用 jq,将对象数组转换为具有命名键的对象

如何在 JAVA 中对 JSONArray 进行排序

从 JSON 中 Select 不同的值

类型是接口或抽象类,不能实例化

如何在 postgresql 9.3 中循环 JSON 数组

play 2 JSON 格式中缺少的属性的默认值