使用JSON序列化一个以元组为键的字典

73

在Python中,有没有一种方法可以将使用元组作为键的字典序列化?

例如:

a = {(1, 2): 'a'}

仅使用json.dumps(a)会引发此错误:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/json/__init__.py", line 230, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python2.6/json/encoder.py", line 367, in encode
    chunks = list(self.iterencode(o))
  File "/usr/lib/python2.6/json/encoder.py", line 309, in _iterencode
    for chunk in self._iterencode_dict(o, markers):
  File "/usr/lib/python2.6/json/encoder.py", line 268, in _iterencode_dict
    raise TypeError("key {0!r} is not a string".format(key))
TypeError: key (1, 2) is not a string

1
可能是使用JSON编码元组的最佳方法的重复问题。 - agf
这个回答解决了你的问题吗?最佳方法使用JSON编码元组 - David Buck
这个回答解决了您的问题吗?使用JSON编码元组的最佳方法 - undefined
11个回答

46

你无法将此作为json序列化,因为json对字典键的定义比Python更不灵活。

你可以将映射转换为键值对序列,类似于这样:

import json
def remap_keys(mapping):
    return [{'key':k, 'value': v} for k, v in mapping.iteritems()]
... 
json.dumps(remap_keys({(1, 2): 'foo'}))
>>> '[{"value": "foo", "key": [1, 2]}]'

2
你能否在加载JSON后添加代码来取消重新映射键?这将使答案更加完整。 - Kvothe
2
请注意,Python 3 将 dict.iteritems 改名为 dict.items - Tms91
@kvothe 我已经添加了一个回答,也解决了你的请求。 - Tms91

15
from json import loads, dumps
from ast import literal_eval

x = {(0, 1): 'la-la la', (0, 2): 'extricate'}

# save: convert each tuple key to a string before saving as json object
s = dumps({str(k): v for k, v in x.items()})

# load in two stages:
# (i) load json object
obj = loads(s)

# (ii) convert loaded keys from string back to tuple
d = {literal_eval(k): v for k, v in obj.items()}

请参见https://dev59.com/GWct5IYBdhLWcg3wHZ5q#12337657


1
这也适用于字典作为键!或者任何满足 literal_eval(str(x)) == x 的情况!需要注意的是,literal_eval() 存在安全风险,它会执行任意代码,因此只有在信任要加载的 JSON 字符串时才使用。您可以通过将 str(k) 替换为 json.dumps(k) 进行保存,将 literal_eval(k) 替换为 tuple(json.loads(k)) 进行加载来避免此问题。 - Nick Crews
这是不安全的,如果您不能信任JSON输入。 - deed02392

10

你可以直接使用str((1,2))作为键,因为json只期望键是字符串,但如果你使用这个,你必须使用a[str((1,2))]来获取值。


1
如果我们想保留键入项目的方式,我认为这是最佳选择。 - otayeby

8

JSON只支持字符串作为键。您需要选择一种方式将这些元组表示为字符串。


这并不完全符合问题的要求。Python类型映射到JSON键必须是str、int、float、bool或None,因此OP只需要弄清楚如何映射到其中一种类型即可。 - physincubus

6

这个解决方案:

  • 避免了eval()的安全风险。
  • 简短明了。
  • 可作为保存和加载函数进行复制粘贴。
  • 保留元组结构作为键,以防手动编辑JSON时出错。
  • 在元组表示中添加了丑陋的\",比其他str()/eval()方法更糟糕。
  • 只能处理嵌套字典的一级元组键(截至本文撰写时,没有其他解决方案能够做得更好)。
def json_dumps_tuple_keys(mapping):
    string_keys = {json.dumps(k): v for k, v in mapping.items()}
    return json.dumps(string_keys)

def json_loads_tuple_keys(string):
    mapping = json.loads(string)
    return {tuple(json.loads(k)): v for k, v in mapping.items()}

m = {(0,"a"): "first", (1, "b"): [9, 8, 7]}
print(m)      # {(0, 'a'): 'first', (1, 'b'): [9, 8, 7]}
s = json_dumps_tuple_keys(m)
print(s)      # {"[0, \"a\"]": "first", "[1, \"b\"]": [9, 8, 7]}
m2 = json_loads_tuple_keys(s)
print(m2)     # {(0, 'a'): 'first', (1, 'b'): [9, 8, 7]}
print(m==m2)  # True

3

JSON只能接受字符串作为字典的键,你可以将元组键替换为字符串,如下所示。

with open("file", "w") as f:
    k = dic.keys() 
    v = dic.values() 
    k1 = [str(i) for i in k]
    json.dump(json.dumps(dict(zip(*[k1,v]))),f) 

当您需要阅读它时,您可以使用以下方法将键更改回元组

with open("file", r) as f:
    data = json.load(f)
    dic = json.loads(data)
    k = dic.keys() 
    v = dic.values() 
    k1 = [eval(i) for i in k] 
    return dict(zip(*[k1,v])) 

2

这里有两个函数可以用来将一个 以元组为键的字典 转换成一个 以键和值为键的 JSON 数组,然后再将其反向转换回来。

import json

def json_dumps_dict_having_tuple_as_key(dict_having_tuple_as_key):
    if not isinstance(dict_having_tuple_as_key, dict):
        raise Exception('Error using json_dumps_dict_having_tuple_as_key: The input variable is not a dictionary.')  
    list_of_dicts_having_key_and_value_as_keys = [{'key': k, 'value': v} for k, v in dict_having_tuple_as_key.items()]
    json_array_having_key_and_value_as_keys = json.dumps(list_of_dicts_having_key_and_value_as_keys)
    return json_array_having_key_and_value_as_keys

def json_loads_dictionary_split_into_key_and_value_as_keys_and_underwent_json_dumps(json_array_having_key_and_value_as_keys):
    list_of_dicts_having_key_and_value_as_keys = json.loads(json_array_having_key_and_value_as_keys)
    if not all(['key' in diz for diz in list_of_dicts_having_key_and_value_as_keys]) and all(['value' in diz for diz in list_of_dicts_having_key_and_value_as_keys]):
        raise Exception('Error using json_loads_dictionary_split_into_key_and_value_as_keys_and_underwent_json_dumps: at least one dictionary in list_of_dicts_having_key_and_value_as_keys ismissing key "key" or key "value".')
    dict_having_tuple_as_key = {}
    for dict_having_key_and_value_as_keys in list_of_dicts_having_key_and_value_as_keys:
        dict_having_tuple_as_key[ tuple(dict_having_key_and_value_as_keys['key']) ] = dict_having_key_and_value_as_keys['value']
    return dict_having_tuple_as_key

使用示例:

my_dict = {
    ('1', '1001', '2021-12-21', '1', '484'): {"name": "Carl", "surname": "Black", "score": 0},
    ('1', '1001', '2021-12-22', '1', '485'): {"name": "Joe", "id_number": 134, "percentage": 11}
}

my_json = json_dumps_dict_having_tuple_as_key(my_dict)
print(my_json)
[{'key': ['1', '1001', '2021-12-21', '1', '484'], 'value': {'name': 'Carl', 'surname': 'Black', 'score': 0}}, 
 {'key': ['1', '1001', '2021-12-22', '1', '485'],  'value': {'name': 'Joe', 'id_number': 134, 'percentage': 11}}]
my_dict_reconverted = json_loads_dictionary_split_into_key_and_value_as_keys_and_underwent_json_dumps(my_json)
print(my_dict_reconverted)
{('1', '1001', '2021-12-21', '1', '484'): {'name': 'Carl', 'surname': 'Black', 'score': 0}, 
 ('1', '1001', '2021-12-22', '1', '485'): {'name': 'Joe', 'id_number': 134, 'percentage': 11}}
# proof of working 1

my_dict == my_dict_reconverted
True
# proof of working 2

my_dict == json_loads_dictionary_split_into_key_and_value_as_keys_and_underwent_json_dumps(
json_dumps_dict_having_tuple_as_key(my_dict)
)
True
(使用 @SingleNegationElimination 表达的概念来回答 @Kvothe 的评论)

1
一个好的例子展示了通过不良命名如何使得一些令人惊叹的功能变得难以使用。这些函数真的很棒,如果你简化名称,例如将变量“remapped_dict”和函数“load_dict”简化,并且删除不属于问题的断言,我相信它就已经有50个更新了。 - KingOtto

2

以下是一种实现方式。需要在主字典解码后对密钥进行json解码,并重新排序整个字典,但这是可行的:

    import json

    def jsonEncodeTupleKeyDict(data):
        ndict = dict()
        # creates new dictionary with the original tuple converted to json string
        for key,value in data.iteritems():
            nkey = json.dumps(key)
            ndict[nkey] =  value

        # now encode the new dictionary and return that
        return json.dumps(ndict)

    def main():
        tdict = dict()
        for i in range(10):
            key = (i,"data",5*i)
            tdict[key] = i*i

        try:
            print json.dumps(tdict)
        except TypeError,e:
            print "JSON Encode Failed!",e

        print jsonEncodeTupleKeyDict(tdict)

    if __name__ == '__main__':
        main()

我不保证这种方法的效率。 我需要将一些游戏手柄映射数据保存到文件中。 我希望使用一些能够创建半人类可读格式的东西,以便在需要时进行编辑。


1
你实际上不能将元组作为键序列化为JSON,但是你可以将元组转换为字符串并在反序列化文件后恢复它。
with_tuple = {(0.1, 0.1): 3.14} ## this will work in python but is not serializable in json
{(0.1, 0.1): 3.14}

但您无法使用json序列化它。但是,您可以使用
with_string = {str((0.1, 0.1))[1:-1]: 3.14} ## the expression [1,-1] removes the parenthesis surrounding the tuples in python. 

{'0.1, 0.1': 3.14} # This is serializable

通过一些欺骗操作,在反序列化整个文件之后,您可以单独将每个键(作为字符串)处理,从而恢复原始元组。
tuple(json.loads("["+'0.1, 0.1'+"]")) ## will recover the tuple from string
(0.1, 0.1)

使用 json.loads 将字符串转换为元组有点过载,但这样做是可行的。封装它,你就完成了。
祝平安,编码愉快!
Nicolas

0
def stringify_keys(d):
    if isinstance(d, dict):
        return {str(k): stringify_keys(v) for k, v in d.items()}
    if isinstance(d, (list, tuple)):
        return type(d)(stringify_keys(v) for v in d)
    return d

json.dumps(stringify_keys(mydict))

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