Module json de Python, convertit les clés du dictionnaire int en chaînes

132

J'ai trouvé que lorsque ce qui suit est exécuté, le module json de python (inclus depuis 2.6) convertit les clés du dictionnaire int en chaînes.

>>> import json
>>> releases = {1: "foo-v0.1"}
>>> json.dumps(releases)
'{"1": "foo-v0.1"}'

Existe-t-il un moyen simple de conserver la clé en tant qu'int, sans avoir besoin d'analyser la chaîne lors du vidage et du chargement. Je pense qu'il serait possible d'utiliser les hooks fournis par le module json, mais encore une fois, cela nécessite toujours une analyse. Y a-t-il peut-être un argument que j'ai négligé? bravo, chaz

Sous-question: Merci pour les réponses. Vu que json fonctionne comme je le craignais, y a-t-il un moyen facile de transmettre le type de clé en analysant peut-être la sortie des vidages? Je dois également noter que le code faisant le dumping et le code téléchargeant l'objet json à partir d'un serveur et le chargeant, sont tous deux écrits par moi.

Charles Ritchie
la source
23
Les clés json doivent être des chaînes
tonfa

Réponses:

87

C'est l'une de ces différences subtiles entre les différentes collections de cartographie qui peuvent vous mordre. JSON traite les clés comme des chaînes; Python prend en charge des clés distinctes ne différant que par le type.

En Python (et apparemment en Lua), les clés d'un mappage (dictionnaire ou table, respectivement) sont des références d'objet. En Python, ils doivent être des types immuables ou des objets qui implémentent une __hash__méthode. (La documentation Lua suggère qu'il utilise automatiquement l'ID de l'objet comme un hachage / clé même pour les objets mutables et s'appuie sur l'internalisation de chaînes pour garantir que les chaînes équivalentes correspondent aux mêmes objets).

En Perl, Javascript, awk et de nombreux autres langages, les clés pour les hachages, les tableaux associatifs ou tout ce qu'ils appellent pour le langage donné, sont des chaînes (ou "scalaires" en Perl). En perl se $foo{1}, $foo{1.0}, and $foo{"1"}trouvent toutes les références au même mappage en %foo--- la clé est évaluée comme un scalaire!

JSON a commencé comme une technologie de sérialisation Javascript. (JSON signifie J ava S cript O bjet N ottaison.) Bien entendu , il met en oeuvre la sémantique pour sa notation de correspondance qui sont compatibles avec la sémantique de mappage.

Si les deux extrémités de votre sérialisation doivent être Python, vous feriez mieux d'utiliser des pickles. Si vous avez vraiment besoin de les reconvertir de JSON en objets Python natifs, je suppose que vous avez plusieurs choix. Vous pouvez d'abord essayer ( try: ... except: ...) de convertir n'importe quelle clé en nombre en cas d'échec de la recherche dans le dictionnaire. Sinon, si vous ajoutez du code à l'autre extrémité (le sérialiseur ou le générateur de ces données JSON), vous pouvez lui demander d'effectuer une sérialisation JSON sur chacune des valeurs de clé - en les fournissant sous forme de liste de clés. (Ensuite, votre code Python itérerait d'abord sur la liste des clés, les instanciant / désérialisant en objets Python natifs ... puis les utiliserait pour accéder aux valeurs hors du mappage).

Jim Dennis
la source
1
Merci pour ça. Malheureusement, je ne peux pas utiliser Pickle, mais votre idée avec la liste est excellente. Va mettre en œuvre cela maintenant, bravo pour l'idée.
Charles Ritchie
1
(Incidemment, en Python 1, 1L (entier long) et 1.0 mappent sur la même clé; mais "1" (une chaîne) ne correspond pas à 1 (entier) ou 1.0 (flottant) ou 1L (entier long ).
Jim Dennis
5
Soyez prudent avec la recommandation d'utiliser Pickle. Pickle peut entraîner l'exécution de code arbitraire, donc si la source des données que vous désérialisez n'est pas intrinsèquement digne de confiance, vous devez vous en tenir à un protocole de sérialisation «sûr» comme JSON. Gardez également à l'esprit qu'au fur et à mesure que la portée des projets s'étend, les fonctions auxquelles vous vous attendiez ne recevraient que des entrées fiables commencent à recevoir des entrées fournies par l'utilisateur, et les considérations de sécurité ne sont pas toujours réexaminées.
AusIV
55

Non, il n'existe pas de clé numérique en JavaScript. Toutes les propriétés d'objet sont converties en String.

var a= {1: 'a'};
for (k in a)
    alert(typeof k); // 'string'

Cela peut conduire à des comportements d'apparence curieuse:

a[999999999999999999999]= 'a'; // this even works on Array
alert(a[1000000000000000000000]); // 'a'
alert(a['999999999999999999999']); // fail
alert(a['1e+21']); // 'a'

Les objets JavaScript ne sont pas vraiment des mappages appropriés comme vous le comprenez dans des langages comme Python, et l'utilisation de clés qui ne sont pas String entraîne des bizarreries. C'est pourquoi JSON écrit toujours explicitement les clés sous forme de chaînes, même là où cela ne semble pas nécessaire.

bobince
la source
1
Pourquoi n'est pas 999999999999999999999converti en '999999999999999999999'?
Piotr Dobrogost
4
@PiotrDobrogost JavaScript (comme de nombreux langages) ne peut pas stocker des nombres arbitrairement grands. Le Numbertype est une valeur à double virgule flottante IEEE 754 : vous obtenez 53 bits de mantisse, vous pouvez donc stocker jusqu'à 2⁵³ (9007199254740992) avec une précision entière; au-delà, les entiers arrondiront à d'autres valeurs (d'où 9007199254740993 === 9007199254740992). 999999999999999999999 est arrondi à 1000000000000000000000, pour lequel la toStringreprésentation par défaut est 1e+21.
bobince
22

Vous pouvez également essayer de convertir le dictionnaire en une liste au format [(k1, v1), (k2, v2)] tout en l'encodant à l'aide de json, et en le reconvertissant en dictionnaire après l'avoir décodé.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Je pense que cela nécessitera plus de travail, comme avoir une sorte de drapeau pour identifier tous les paramètres à convertir en dictionnaire après l'avoir décodé à partir de json.

Ashish
la source
Bonne solution pour les objets dict sans objets dict imbriqués!
Tom Yu
15

Répondre à votre sous-question:

Cela peut être accompli en utilisant json.loads(jsonDict, object_hook=jsonKeys2int)

def jsonKeys2int(x):
    if isinstance(x, dict):
            return {int(k):v for k,v in x.items()}
    return x

Cette fonction fonctionnera également pour les dictionnaires imbriqués et utilise une compréhension de dict.

Si vous souhaitez également convertir les valeurs, utilisez:

def jsonKV2int(x):
    if isinstance(x, dict):
            return {int(k):(int(v) if isinstance(v, unicode) else v) for k,v in x.items()}
    return x

Qui teste l'instance des valeurs et ne les casse que s'il s'agit d'objets chaînes (unicode pour être exact).

Les deux fonctions supposent que les clés (et les valeurs) sont des entiers.

Grâce à:

Comment utiliser if / else dans une compréhension de dictionnaire?

Convertir une clé de chaîne en int dans un dictionnaire

Murmel
la source
C'était génial. Dans mon cas, le décapage ne peut pas être utilisé, donc j'enregistre les tripes d'un objet en utilisant JSON via la conversion en byte_array afin que je puisse utiliser la compression. J'ai des clés mixtes, donc je viens de modifier votre exemple pour ignorer une ValueError lorsque la clé n'est pas convertible en un int
minillinim
11

J'ai été mordu par le même problème. Comme d'autres l'ont souligné, dans JSON, les clés de mappage doivent être des chaînes. Vous pouvez faire deux choses. Vous pouvez utiliser une bibliothèque JSON moins stricte, comme demjson , qui autorise les chaînes entières. Si aucun autre programme (ou aucun autre dans d'autres langues) ne va le lire, alors vous devriez être d'accord. Ou vous pouvez utiliser un autre langage de sérialisation. Je ne suggérerais pas de cornichon. Il est difficile à lire et n'est pas conçu pour être sécurisé . Au lieu de cela, je suggérerais YAML, qui est (presque) un sur-ensemble de JSON, et autorise les clés entières. (Au moins PyYAML le fait.)

AFoglie
la source
2

Convertissez le dictionnaire en chaîne en utilisant str(dict), puis reconvertissez-le en dict en procédant comme suit:

import ast
ast.literal_eval(string)
Hzzkygcs
la source
1

Voici ma solution! Je l'ai utilisé object_hook, c'est utile quand on a imbriquéjson

>>> import json
>>> json_data = '{"1": "one", "2": {"-3": "minus three", "4": "four"}}'
>>> py_dict = json.loads(json_data, object_hook=lambda d: {int(k) if k.lstrip('-').isdigit() else k: v for k, v in d.items()})

>>> py_dict
{1: 'one', 2: {-3: 'minus three', 4: 'four'}}

Il existe un filtre uniquement pour analyser la clé json en int. Vous pouvez également utiliser un int(v) if v.lstrip('-').isdigit() else vfiltre pour la valeur json.

GooDeeJaY
la source
1

J'ai fait une extension très simple de la réponse de Murmel qui, je pense, fonctionnera sur un dictionnaire assez arbitraire (y compris imbriqué) en supposant qu'il puisse être vidé par JSON en premier lieu. Toutes les clés qui peuvent être interprétées comme des entiers seront converties en int. Il ne fait aucun doute que ce n'est pas très efficace, mais cela fonctionne pour mes besoins de stockage et de chargement à partir de chaînes json.

def convert_keys_to_int(d: dict):
    new_dict = {}
    for k, v in d.items():
        try:
            new_key = int(k)
        except ValueError:
            new_key = k
        if type(v) == dict:
            v = _convert_keys_to_int(v)
        new_dict[new_key] = v
    return new_dict

En supposant que toutes les clés du dict original sont des entiers si elles peuvent être transtypées en int, cela renverra le dictionnaire d'origine après le stockage en tant que json. par exemple

>>>d = {1: 3, 2: 'a', 3: {1: 'a', 2: 10}, 4: {'a': 2, 'b': 10}}
>>>convert_keys_to_int(json.loads(json.dumps(d)))  == d
True
Tim enfant
la source
-1

Vous pouvez écrire vous- json.dumpsmême, voici un exemple de djson : encoder.py . Vous pouvez l'utiliser comme ceci:

assert dumps({1: "abc"}) == '{1: "abc"}'
damnever
la source