El módulo json de Python, convierte las claves del diccionario int en cadenas

130

He descubierto que cuando se ejecuta lo siguiente, el módulo json de python (incluido desde 2.6) convierte las claves del diccionario int en cadenas.

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

¿Hay alguna manera fácil de preservar la clave como int, sin necesidad de analizar la cadena al volcar y cargar? Creo que sería posible usar los ganchos provistos por el módulo json, pero nuevamente esto aún requiere análisis. ¿Hay alguna discusión que haya pasado por alto? salud, chaz

Subpregunta: Gracias por las respuestas. Viendo que json funciona como temía, ¿hay una manera fácil de transmitir el tipo de clave al analizar la salida de los volcados? También debo tener en cuenta que el código que realiza el volcado y el código que descarga el objeto json de un servidor y lo carga, ambos están escritos por mí.

Charles Ritchie
fuente
23
json keys tienen que ser cadenas
tonfa

Respuestas:

86

Esta es una de esas sutiles diferencias entre varias colecciones de mapas que pueden morderte. JSON trata las claves como cadenas; Python admite teclas distintas que difieren solo en tipo.

En Python (y aparentemente en Lua) las claves de un mapeo (diccionario o tabla, respectivamente) son referencias de objetos. En Python deben ser tipos inmutables, o deben ser objetos que implementen un__hash__ método. (Los documentos de Lua sugieren que usa automáticamente la ID del objeto como un hash / clave incluso para objetos mutables y se basa en el internamiento de cadenas para garantizar que las cadenas equivalentes se asignen a los mismos objetos).

En Perl, Javascript, awk y muchos otros idiomas, las claves para hash, matrices asociativas o como se llamen para el idioma dado son cadenas (o "escalares" en Perl). En perl $foo{1}, $foo{1.0}, and $foo{"1"}están todas las referencias a la misma asignación en %foo--- se evalúa la clave como escalar!

JSON comenzó como una tecnología de serialización Javascript. (JSON significa J ava S cript O bject N otation.) Naturalmente implementa semántica para su notación de mapeo que es consistente con su semántica de mapeo.

Si ambos extremos de su serialización van a ser Python, entonces sería mejor usar encurtidos. Si realmente necesita convertirlos de JSON a objetos nativos de Python, supongo que tiene un par de opciones. Primero, puede intentar ( try: ... except: ...) convertir cualquier clave en un número en caso de un error en la búsqueda del diccionario. Alternativamente, si agrega código al otro extremo (el serializador o generador de estos datos JSON), puede hacer que realice una serialización JSON en cada uno de los valores clave, proporcionándolos como una lista de claves. (Luego, su código Python primero iteraría sobre la lista de claves, instanciando / deserializándolas en objetos nativos de Python ... y luego las usaría para acceder a los valores fuera de la asignación).

Jim Dennis
fuente
1
Gracias por eso. Desafortunadamente no puedo usar Pickle, pero tu idea con la lista es genial. Implementará eso ahora, aplaude la idea.
Charles Ritchie
1
(Por cierto, en Python 1, 1L (entero largo) y 1.0 se asignan a la misma clave; pero "1" (una cadena) no se asigna a 1 (entero) o 1.0 (flotante) o 1L (entero largo )
Jim Dennis
55
Tenga cuidado con la recomendación de usar Pickle. Pickle puede provocar la ejecución de código arbitrario, por lo que si la fuente de los datos que está deserializando no es inherentemente confiable, debe apegarse a un protocolo de serialización "seguro" como JSON. También tenga en cuenta que a medida que se expande el alcance de los proyectos, a veces las funciones que usted esperaba solo obtendrían entradas confiables y comenzarían a recibir entradas proporcionadas por el usuario, y las consideraciones de seguridad no siempre se revisan.
AusIV
56

No, no existe una clave numérica en JavaScript. Todas las propiedades del objeto se convierten en String.

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

Esto puede conducir a algunos comportamientos curiosos:

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

Los objetos JavaScript no son realmente correlaciones adecuadas, como lo entenderías en lenguajes como Python, y el uso de teclas que no son cadenas da como resultado una rareza. Es por eso que JSON siempre escribe claves explícitamente como cadenas, incluso donde no parece necesario.

bobince
fuente
1
¿Por qué no se 999999999999999999999convierte a '999999999999999999999'?
Piotr Dobrogost
44
@PiotrDobrogost JavaScript (como muchos idiomas) no puede almacenar números arbitrariamente grandes. El Numbertipo es un valor de coma flotante doble IEEE 754 : obtienes 53 bits de mantisa, por lo que puedes almacenar hasta 2⁵³ (9007199254740992) con precisión entera; más allá de eso, los enteros se redondearán a otros valores (por lo tanto, 9007199254740993 === 9007199254740992). 999999999999999999999 se redondea a 1000000000000000000000, para lo cual la toStringrepresentación predeterminada es 1e+21.
bobince
22

Alternativamente, también puede intentar convertir el diccionario a una lista de formato [(k1, v1), (k2, v2)] mientras lo codifica usando json, y volverlo a convertir al diccionario después de volver a decodificarlo.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Creo que esto necesitará más trabajo, como tener algún tipo de indicador para identificar qué parámetros se convertirán al diccionario después de descodificarlo de json.

Ashish
fuente
¡Buena solución para objetos dict sin objetos dict anidados!
Tom Yu
15

Respondiendo su pregunta:

Se puede lograr usando 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

Esta función también funcionará para dictados anidados y utiliza una comprensión dict.

Si también desea emitir los valores, use:

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

Que prueba la instancia de los valores y los convierte solo si son objetos de cadenas (unicode para ser exactos).

Ambas funciones asumen que las claves (y los valores) son enteros.

Gracias a:

¿Cómo usar if / else en un diccionario de comprensión?

Convertir una clave de cadena a int en un diccionario

Murmel
fuente
Esto fue genial. En mi caso, el decapado no se puede usar, así que estoy guardando las tripas de un objeto usando JSON mediante la conversión a un byte_array para poder usar la compresión. Tengo claves mixtas, así que modifiqué su ejemplo para ignorar un ValueError cuando la clave no es convertible a int
minillinim el
11

Me ha picado el mismo problema. Como otros han señalado, en JSON, las claves de mapeo deben ser cadenas. Puedes hacer una de las dos cosas. Puede usar una biblioteca JSON menos estricta, como demjson , que permite cadenas enteras. Si ningún otro programa (o ningún otro en otros idiomas) lo va a leer, entonces debería estar bien. O puede usar un lenguaje de serialización diferente. No sugeriría encurtidos. Es difícil de leer y no está diseñado para ser seguro . En cambio, sugeriría YAML, que es (casi) un superconjunto de JSON, y permite claves enteras. (Al menos PyYAML lo hace).

AFoglia
fuente
2

Convierta el diccionario en cadena mediante el uso str(dict)y luego vuelva a convertirlo en dict haciendo esto:

import ast
ast.literal_eval(string)
Hzzkygcs
fuente
1

¡Aquí está mi solución! Solía object_hook, es útil cuando has anidadojson

>>> 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'}}

Solo hay filtro para analizar la clave json a int. También puede usar el int(v) if v.lstrip('-').isdigit() else vfiltro para el valor json.

GooDeeJaY
fuente
1

Hice una extensión muy simple de la respuesta de Murmel que creo que funcionará en un diccionario bastante arbitrario (incluido el anidado) suponiendo que JSON pueda descartarlo en primer lugar. Cualquier clave que se pueda interpretar como enteros se convertirá en int. Sin duda, esto no es muy eficiente, pero funciona para mis propósitos de almacenar y cargar desde cadenas 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

Suponiendo que todas las claves en el dict original son enteros si se pueden convertir a int, entonces esto devolverá el diccionario original después de almacenarlo como un json. p.ej

>>>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 Child
fuente
-1

Puedes escribir tu json.dumpsmismo, aquí hay un ejemplo de djson : encoder.py . Puedes usarlo así:

assert dumps({1: "abc"}) == '{1: "abc"}'
nunca
fuente