Moduł json w Pythonie konwertuje klucze słownika int na ciągi

130

Zauważyłem, że po uruchomieniu poniższego modułu json w języku Python (dołączony od wersji 2.6) konwertuje klucze słownika int na ciągi.

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

Czy istnieje prosty sposób na zachowanie klucza jako int, bez konieczności analizowania ciągu przy zrzucie i załadowaniu. Uważam, że byłoby to możliwe przy użyciu haków dostarczonych przez moduł json, ale znowu wymaga to analizy. Czy jest jakiś argument, który przeoczyłem? okrzyki, chaz

Pytanie podrzędne: Dziękuję za odpowiedzi. Biorąc pod uwagę, że json działa tak, jak się obawiałem, czy istnieje łatwy sposób na przekazanie typu klucza poprzez może analizę danych wyjściowych zrzutów? Powinienem również zauważyć, że kod wykonujący zrzut i kod pobierający obiekt json z serwera i ładujący go, są napisane przeze mnie.

Charles Ritchie
źródło
23
JSON musi być
ciągiem

Odpowiedzi:

86

To jedna z tych subtelnych różnic między różnymi kolekcjami map, które mogą cię ugryźć. JSON traktuje klucze jako ciągi znaków; Python obsługuje różne klucze różniące się tylko typem.

W Pythonie (i najwyraźniej w Lua) kluczami do mapowania (odpowiednio słownika lub tabeli) są odwołania do obiektów. W Pythonie muszą być typami niezmiennymi lub muszą być obiektami implementującymi __hash__metodę. (Dokumentacja Lua sugeruje, że automatycznie używa identyfikatora obiektu jako skrótu / klucza nawet dla obiektów podlegających zmianom i polega na internowaniu ciągów, aby zapewnić, że równoważne ciągi są mapowane na te same obiekty).

W Perlu, Javascript, awk i wielu innych językach klucze do skrótów, tablic asocjacyjnych lub czegokolwiek, co się nazywa w danym języku, to łańcuchy (lub „skalary” w Perlu). W perlu $foo{1}, $foo{1.0}, and $foo{"1"}są wszystkie odniesienia do tego samego mapowania w %foo--- klucz jest oceniany jako skalar!

JSON powstał jako technologia serializacji JavaScript. (JSON oznacza J Ava S CRIPT O bject N otation). Naturalnie realizuje semantyki dla jego zapis mapowania, które są zgodne z jego semantyki mapowania.

Jeśli oba końce serializacji mają być w Pythonie, lepiej byłoby użyć pikli. Jeśli naprawdę potrzebujesz przekonwertować je z powrotem z JSON na natywne obiekty Pythona, myślę, że masz kilka możliwości. Najpierw możesz spróbować ( try: ... except: ...), aby przekonwertować dowolny klucz na liczbę w przypadku niepowodzenia wyszukiwania w słowniku. Alternatywnie, jeśli dodasz kod na drugim końcu (serializator lub generator tych danych JSON), możesz zlecić wykonanie serializacji JSON na każdej z wartości klucza - dostarczając je jako listę kluczy. (Następnie twój kod Pythona najpierw iteruje listę kluczy, tworząc instancję / deserializując je w natywne obiekty Pythona ... a następnie użyje ich do uzyskania dostępu do wartości poza mapowaniem).

Jim Dennis
źródło
1
Dziękuję za to. Niestety nie mogę użyć Pickle, ale Twój pomysł z listą jest świetny. Zrealizuję to teraz, okrzyki dla pomysłu.
Charles Ritchie
1
(Nawiasem mówiąc, w Pythonie 1, 1L (długa liczba całkowita) i 1.0 są mapowane na ten sam klucz; ale „1” (ciąg znaków) nie jest mapowane na to samo, co 1 (liczba całkowita) lub 1,0 (liczba zmiennoprzecinkowa) lub 1L (długa liczba całkowita )
Jim Dennis
5
Zachowaj ostrożność przy zalecaniu używania Pickle. Pickle może spowodować wykonanie dowolnego kodu, więc jeśli źródło deserializowanych danych nie jest z natury godne zaufania, należy trzymać się „bezpiecznego” protokołu serializacji, takiego jak JSON. Należy również pamiętać, że wraz z rozszerzaniem się zakresu projektów, czasami funkcje, których oczekiwałeś, otrzymają tylko zaufane dane wejściowe, zaczną otrzymywać dane wejściowe dostarczone przez użytkownika, a kwestie bezpieczeństwa nie zawsze są ponownie analizowane.
AusIV,
56

Nie, w JavaScript nie ma czegoś takiego jak klawisz numeryczny. Wszystkie właściwości obiektu są konwertowane na String.

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

Może to prowadzić do dziwnych, pozornych zachowań:

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

Obiekty JavaScript nie są tak naprawdę poprawnymi odwzorowaniami, jak można to rozumieć w językach takich jak Python, a używanie kluczy, które nie są ciągami znaków, prowadzi do dziwności. Dlatego JSON zawsze jawnie zapisuje klucze jako ciągi, nawet jeśli nie wydaje się to konieczne.

bobince
źródło
1
Dlaczego nie jest 999999999999999999999konwertowany na '999999999999999999999'?
Piotr Dobrogost
4
@PiotrDobrogost JavaScript (podobnie jak wiele języków) nie może przechowywać dowolnie dużych liczb. NumberTyp jest IEEE 754 podwójna wartość zmiennoprzecinkowa: masz 53 bitów mantysy, dzięki czemu można przechowywać do 2⁵³ (9007199254740992) z dokładnością do liczby całkowitej; poza tą liczbą całkowitą zaokrągli się do innych wartości (stąd 9007199254740993 === 9007199254740992). 999999999999999999999 zaokrągla liczbę do 1000000000000000000000, dla której domyślną toStringreprezentacją jest 1e+21.
bobince
22

Alternatywnie możesz również spróbować przekonwertować słownik na listę formatu [(k1, v1), (k2, v2)] podczas kodowania go przy użyciu json i przekonwertować go z powrotem do słownika po zdekodowaniu.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Uważam, że będzie to wymagało trochę więcej pracy, na przykład posiadania jakiejś flagi, aby zidentyfikować wszystkie parametry, które mają zostać przekonwertowane na słownik po zdekodowaniu z powrotem z json.

Ashish
źródło
Dobre rozwiązanie dla obiektów Dict bez zagnieżdżonych obiektów Dict!
Tom Yu
15

Odpowiadając na Twoje pytanie:

Można to osiągnąć za pomocą 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

Ta funkcja będzie również działać w przypadku poleceń zagnieżdżonych i używa rozumienia dyktowania.

Jeśli chcesz również rzutować wartości, użyj:

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

Który testuje instancję wartości i rzuca je tylko wtedy, gdy są one obiektami łańcuchowymi (dokładnie w formacie Unicode).

Obie funkcje przyjmują, że klucze (i wartości) są liczbami całkowitymi.

Dzięki:

Jak używać if / else w rozumieniu słownikowym?

Przekonwertuj klucz ciągu na int w słowniku

Murmel
źródło
To było świetne. W moim przypadku nie można użyć wytrawiania, więc oszczędzam wnętrzności obiektu za pomocą JSON poprzez konwersję do byte_array, aby móc użyć kompresji. Mam mieszane klucze, więc właśnie zmodyfikowałem twój przykład, aby zignorować ValueError, gdy klucza nie można
zamienić
11

Ugryzł mnie ten sam problem. Jak zauważyli inni, w JSON klucze mapowania muszą być ciągami. Możesz zrobić jedną z dwóch rzeczy. Możesz użyć mniej rygorystycznej biblioteki JSON, takiej jak demjson , która zezwala na ciągi liczb całkowitych. Jeśli żadne inne programy (lub żadne inne programy w innych językach) nie będą go czytać, wszystko powinno być w porządku. Możesz też użyć innego języka serializacji. Nie sugerowałbym marynaty. Jest trudny do odczytania i nie jest zaprojektowany jako bezpieczny . Zamiast tego sugerowałbym YAML, który jest (prawie) nadzbiorem JSON i dopuszcza klucze całkowite. (A przynajmniej PyYAML .)

AFoglia
źródło
2

Przekonwertuj słownik na ciąg przy użyciu, str(dict)a następnie przekonwertuj go z powrotem na dict, wykonując następujące czynności:

import ast
ast.literal_eval(string)
Hzzkygcs
źródło
1

Oto moje rozwiązanie! Użyłem object_hook, jest to przydatne, gdy masz zagnieżdżonejson

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

Istnieje filtr tylko do analizowania klucza json do int. Możesz również użyć int(v) if v.lstrip('-').isdigit() else vfiltru dla wartości json.

GooDeeJaY
źródło
1

Zrobiłem bardzo proste rozszerzenie odpowiedzi Murmela, które, jak sądzę, będzie działać na dość arbitralnym słowniku (w tym zagnieżdżonym), zakładając, że może zostać zrzucone przez JSON w pierwszej kolejności. Wszystkie klucze, które można zinterpretować jako liczby całkowite, zostaną rzutowane na int. Bez wątpienia nie jest to zbyt wydajne, ale działa na potrzeby przechowywania i ładowania z ciągów 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

Zakładając, że wszystkie klucze w oryginalnym dict są liczbami całkowitymi, jeśli można je rzutować na int, to zwróci oryginalny słownik po zapisaniu jako json. na przykład

>>>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
źródło
-1

Możesz napisać swój json.dumpssamodzielnie, oto przykład z djson : encoder.py . Możesz go używać w ten sposób:

assert dumps({1: "abc"}) == '{1: "abc"}'
cholera
źródło