[譯] json — JavaScript 物件表示法

snowyYU發表於2019-02-27

json — JavaScript 物件表示法

目的:實現 Python 物件和 JSON 字串間的相互轉化。

json 模組提供了一個類似於 pickle 的 API,用於將記憶體中的 Python 物件轉換為 JavaScript Object Notation(JSON)的序列化表示形式。相較於 pickle,JSON 優勢之一就是被許多語言實現和應用(特別是 JavaScript)。它被廣泛用於 REST API 中Web伺服器和客戶端之間的通訊,此外也可用於其他應用程式間的通訊。

編碼和解碼簡單的資料型別

JSON 編碼器原生支援 Python 的基本型別 (str, int, float, list, tuple, 和 dict).

# json_simple_types.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]
print(`DATA:`, repr(data))

data_string = json.dumps(data)
print(`JSON:`, data_string)
複製程式碼

值的編碼方式看起來類似於 Python 的 repr() 輸出。

$ python3 json_simple_types.py

DATA: [{`c`: 3.0, `b`: (2, 4), `a`: `A`}]
JSON: [{"c": 3.0, "b": [2, 4], "a": "A"}]
複製程式碼

編碼之後再解碼,將可能得到並不完全相同的物件型別。

# json_simple_types_decode.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]
print(`DATA   :`, data)

data_string = json.dumps(data)
print(`ENCODED:`, data_string)

decoded = json.loads(data_string)
print(`DECODED:`, decoded)

print(`ORIGINAL:`, type(data[0][`b`]))
print(`DECODED :`, type(decoded[0][`b`]))
複製程式碼

特別注意,元組會轉化成列表。

$ python3 json_simple_types_decode.py

DATA   : [{`c`: 3.0, `b`: (2, 4), `a`: `A`}]
ENCODED: [{"c": 3.0, "b": [2, 4], "a": "A"}]
DECODED: [{`c`: 3.0, `b`: [2, 4], `a`: `A`}]
ORIGINAL: <class `tuple`>
DECODED : <class `list`>
複製程式碼

可讀性 vs. 緊湊型輸出

相較於pickle,JSON 可讀性更好。 dumps() 函式接收若干引數來優化輸出的可讀性。例如,sort_keys引數告訴編碼器以排序而不是隨機順序輸出字典的鍵的值。

# json_sort_keys.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]
print(`DATA:`, repr(data))

unsorted = json.dumps(data)
print(`JSON:`, json.dumps(data))
print(`SORT:`, json.dumps(data, sort_keys=True))

first = json.dumps(data, sort_keys=True)
second = json.dumps(data, sort_keys=True)

print(`UNSORTED MATCH:`, unsorted == first)
print(`SORTED MATCH  :`, first == second)
複製程式碼

有序輸出,可讀性自然比較高,並且在測試中容易對 JSON 的輸出進行比較。

$ python3 json_sort_keys.py

DATA: [{`c`: 3.0, `b`: (2, 4), `a`: `A`}]
JSON: [{"c": 3.0, "b": [2, 4], "a": "A"}]
SORT: [{"a": "A", "b": [2, 4], "c": 3.0}]
UNSORTED MATCH: False
SORTED MATCH  : True
複製程式碼

對於高度巢狀的資料結構,請為 indent 指定一個值,以便輸出結構更加清晰的格式。

# json_indent.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]
print(`DATA:`, repr(data))

print(`NORMAL:`, json.dumps(data, sort_keys=True))
print(`INDENT:`, json.dumps(data, sort_keys=True, indent=2))
複製程式碼

當 indent 是一個非負整數時,其輸出更接近 pprint 的輸出,其縮排的空格數與傳入的 indent 值相同,展示了清晰的資料結構。

$ python3 json_indent.py

DATA: [{`c`: 3.0, `b`: (2, 4), `a`: `A`}]
NORMAL: [{"a": "A", "b": [2, 4], "c": 3.0}]
INDENT: [
  {
    "a": "A",
    "b": [
      2,
      4
    ],
    "c": 3.0
  }
]
複製程式碼

雖然輸出的資料結構更清晰,但增加了傳輸相同資料量所需的位元組數,因此它不適用於生產環境。實際上,可以通過設定輸出的 separators 引數,使其編碼後的值比預設情況下更緊湊。

# json_compact_encoding.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]
print(`DATA:`, repr(data))

print(`repr(data)             :`, len(repr(data)))

plain_dump = json.dumps(data)
print(`dumps(data)            :`, len(plain_dump))

small_indent = json.dumps(data, indent=2)
print(`dumps(data, indent=2)  :`, len(small_indent))

with_separators = json.dumps(data, separators=(`,`, `:`))
print(`dumps(data, separators):`, len(with_separators))
複製程式碼

dumps()separators引數應該是一個包含字串的元組,用於分隔列表中的專案以及字典中的值。預設值是`(`,`,`:`)`。通過消除空佔位符,生成更緊湊的輸出。

$ python3 json_compact_encoding.py

DATA: [{`c`: 3.0, `b`: (2, 4), `a`: `A`}]
repr(data)             : 35
dumps(data)            : 35
dumps(data, indent=2)  : 73
dumps(data, separators): 29
複製程式碼

編碼字典

JSON 期望字典鍵的格式是字串。如果用非字串型別作為鍵編碼字典會產生一個 TypeError。解決該限制的一種方法是使用 skipkeys 引數告訴編碼器跳過非字串鍵:

# json_skipkeys.py

import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0, (`d`,): `D tuple`}]

print(`First attempt`)
try:
    print(json.dumps(data))
except TypeError as err:
    print(`ERROR:`, err)

print()
print(`Second attempt`)
print(json.dumps(data, skipkeys=True))
複製程式碼

沒有丟擲異常,忽略了非字串鍵。

$ python3 json_skipkeys.py

First attempt
ERROR: keys must be a string

Second attempt
[{"c": 3.0, "b": [2, 4], "a": "A"}]
複製程式碼

使用自定義型別

到目前為止,所有的例子都使用了 Pythons 的內建型別,因為這些型別本身就支援 json。此外,通常還需要對自定義類進行編碼,並且有兩種方法可以做到這一點。

將下面的類進行編碼:

# json_myobj.py

class MyObj:

    def __init__(self, s):
        self.s = s

    def __repr__(self):
        return `<MyObj({})>`.format(self.s)
複製程式碼

說個簡單編碼 MyObj 例項的方法:定義一個函式將未知型別轉換為已知型別。類本身不需要進行編碼,所以它只是將一個物件轉換為另一個。

# json_dump_default.py

import json
import json_myobj

obj = json_myobj.MyObj(`instance value goes here`)

print(`First attempt`)
try:
    print(json.dumps(obj))
except TypeError as err:
    print(`ERROR:`, err)


def convert_to_builtin_type(obj):
    print(`default(`, repr(obj), `)`)
    # Convert objects to a dictionary of their representation
    d = {
        `__class__`: obj.__class__.__name__,
        `__module__`: obj.__module__,
    }
    d.update(obj.__dict__)
    return d


print()
print(`With default`)
print(json.dumps(obj, default=convert_to_builtin_type))
複製程式碼

依賴的模組可以正常訪問的情況下,在 convert_to_builtin_type() 中,沒有被 json 識別的類的例項被格式化為具有足夠資訊的字典,然後重新建立該物件.

$ python3 json_dump_default.py

First attempt
ERROR: <MyObj(instance value goes here)> is not JSON serializable

With default
default( <MyObj(instance value goes here)> )
{"s": "instance value goes here", "__module__": "json_myobj",
"__class__": "MyObj"}
複製程式碼

要解析結果並建立一個 MyObj()例項,可以使用 object_hook 引數來呼叫 loads() 以連線解析器,這樣就可以從模組中匯入類並用於建立例項。

為從輸入資料流中解碼出的每個字典呼叫 object_hook,它提供將字典轉換為另一種型別的物件的功能。鉤子函式應該返回撥用應用程式應該接收的物件而不是字典

# json_load_object_hook.py

import json


def dict_to_object(d):
    if `__class__` in d:
        class_name = d.pop(`__class__`)
        module_name = d.pop(`__module__`)
        module = __import__(module_name)
        print(`MODULE:`, module.__name__)
        class_ = getattr(module, class_name)
        print(`CLASS:`, class_)
        args = {
            key: value
            for key, value in d.items()
        }
        print(`INSTANCE ARGS:`, args)
        inst = class_(**args)
    else:
        inst = d
    return inst


encoded_object = ```
    [{"s": "instance value goes here",
      "__module__": "json_myobj", "__class__": "MyObj"}]
    ```

myobj_instance = json.loads(
    encoded_object,
    object_hook=dict_to_object,
)
print(myobj_instance)
複製程式碼

由於 json 將字串值轉換為了 unicode 物件,因此在將其用作類建構函式的關鍵字引數之前,需要將它們重新編碼為 ASCII 字串。

$ python3 json_load_object_hook.py

MODULE: json_myobj
CLASS: <class `json_myobj.MyObj`>
INSTANCE ARGS: {`s`: `instance value goes here`}
[<MyObj(instance value goes here)>]
複製程式碼

類似的鉤子可用於內建的資料型別:整數(parseint),浮點數(parsefloat)和常量(parse constant)。

編碼器和解析器相關的類

除了已經介紹的便利功能之外,json 模組還提供了編碼和解析相關的類。直接使用類可以訪問額外的 API 來定製它們的行為。

JSONEncoder 使用一個可迭代的介面來產生編碼資料的 “塊”,使得它更容易寫入檔案或網路套接字,而無需在記憶體中表示整個資料結構。

# json_encoder_iterable.py

import json

encoder = json.JSONEncoder()
data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]

for part in encoder.iterencode(data):
    print(`PART:`, part)
複製程式碼

輸出以邏輯單位為準,和值的大小無關。

$ python3 json_encoder_iterable.py

PART: [
PART: {
PART: "c"
PART: :
PART: 3.0
PART: ,
PART: "b"
PART: :
PART: [2
PART: , 4
PART: ]
PART: ,
PART: "a"
PART: :
PART: "A"
PART: }
PART: ]
複製程式碼

encode() 方法基本上等同於 ``.join(encoder.iterencode()),此外還有一些預先錯誤檢查。

要對任意物件進行編碼,建議使用與 convert_to_builtin_type() 中使用的類似的實現來過載 default() 方法。

# json_encoder_default.py

import json
import json_myobj


class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        print(`default(`, repr(obj), `)`)
        # Convert objects to a dictionary of their representation
        d = {
            `__class__`: obj.__class__.__name__,
            `__module__`: obj.__module__,
        }
        d.update(obj.__dict__)
        return d


obj = json_myobj.MyObj(`internal data`)
print(obj)
print(MyEncoder().encode(obj))
複製程式碼

和之前的例子輸出相同。

$ python3 json_encoder_default.py

<MyObj(internal data)>
default( <MyObj(internal data)> )
{"s": "internal data", "__module__": "json_myobj", "__class__":
"MyObj"}
複製程式碼

解析文字,將字典轉換為物件比上面提到的實現方法更為複雜,不過差別不大。

# json_decoder_object_hook.py

import json


class MyDecoder(json.JSONDecoder):

    def __init__(self):
        json.JSONDecoder.__init__(
            self,
            object_hook=self.dict_to_object,
        )

    def dict_to_object(self, d):
        if `__class__` in d:
            class_name = d.pop(`__class__`)
            module_name = d.pop(`__module__`)
            module = __import__(module_name)
            print(`MODULE:`, module.__name__)
            class_ = getattr(module, class_name)
            print(`CLASS:`, class_)
            args = {
                key: value
                for key, value in d.items()
            }
            print(`INSTANCE ARGS:`, args)
            inst = class_(**args)
        else:
            inst = d
        return inst


encoded_object = ```
[{"s": "instance value goes here",
  "__module__": "json_myobj", "__class__": "MyObj"}]
```

myobj_instance = MyDecoder().decode(encoded_object)
print(myobj_instance)
複製程式碼

輸出與前面的例子相同。

$ python3 json_decoder_object_hook.py

MODULE: json_myobj
CLASS: <class `json_myobj.MyObj`>
INSTANCE ARGS: {`s`: `instance value goes here`}
[<MyObj(instance value goes here)>]
複製程式碼

使用流和檔案

到目前為止,所有的例子的前提都是假設整個資料結構的編碼版本可以一次儲存在記憶體中。對於包含大量資料的複雜結構,將編碼直接寫入檔案類物件會比較好。load()dump() 函式可以接受檔案類物件的引用作為引數,來進行方便讀寫操作。

# json_dump_file.py

import io
import json

data = [{`a`: `A`, `b`: (2, 4), `c`: 3.0}]

f = io.StringIO()
json.dump(data, f)

print(f.getvalue())
複製程式碼

套接字或常規檔案控制程式碼有著和本示例中使用的 StringIO 緩衝區相同的工作方式。

$ python3 json_dump_file.py

[{"c": 3.0, "b": [2, 4], "a": "A"}]
複製程式碼

儘管它沒有被優化為一次只讀取一部分資料,但 load() 函式仍然提供了一種把輸入流轉換成物件的封裝邏輯方面的好處。

# json_load_file.py

import io
import json

f = io.StringIO(`[{"a": "A", "c": 3.0, "b": [2, 4]}]`)
print(json.load(f))
複製程式碼

就像 dump() 一樣,任何類檔案物件都可以傳遞給 load()

$ python3 json_load_file.py

[{`c`: 3.0, `b`: [2, 4], `a`: `A`}]
複製程式碼

混合資料流

JSONDecoder 包含 raw_decode(),這是一種解碼資料結構後面跟著更多資料的方法,比如帶有尾隨文字的 JSON 資料。返回值是通過對輸入資料進行解碼而建立的物件,以及指示解碼器在何處停止工作的位置索引。

# json_mixed_data.py

import json

decoder = json.JSONDecoder()


def get_decoded_and_remainder(input_data):
    obj, end = decoder.raw_decode(input_data)
    remaining = input_data[end:]
    return (obj, end, remaining)


encoded_object = `[{"a": "A", "c": 3.0, "b": [2, 4]}]`
extra_text = `This text is not JSON.`

print(`JSON first:`)
data = ` `.join([encoded_object, extra_text])
obj, end, remaining = get_decoded_and_remainder(data)

print(`Object              :`, obj)
print(`End of parsed input :`, end)
print(`Remaining text      :`, repr(remaining))

print()
print(`JSON embedded:`)
try:
    data = ` `.join([extra_text, encoded_object, extra_text])
    obj, end, remaining = get_decoded_and_remainder(data)
except ValueError as err:
    print(`ERROR:`, err)
複製程式碼

但是,這隻有在物件出現在輸入的開頭時才有效。

$ python3 json_mixed_data.py

JSON first:
Object              : [{`c`: 3.0, `b`: [2, 4], `a`: `A`}]
End of parsed input : 35
Remaining text      : ` This text is not JSON.`

JSON embedded:
ERROR: Expecting value: line 1 column 1 (char 0)
複製程式碼

命令列中的 JSON

json.tool 模組實現了一個命令列程式,用於重新格式化 JSON 資料以便於閱讀。

[{"a": "A", "c": 3.0, "b": [2, 4]}]
複製程式碼

輸入檔案 example.json 包含一個按字母順序排列的對映。下面的第一個例子顯示按順序重新格式化的資料,第二個例子使用 --sort-keys 在列印輸出之前對對映鍵進行排序。

$ python3 -m json.tool example.json

[
    {
        "a": "A",
        "c": 3.0,
        "b": [
            2,
            4
        ]
    }
]

$ python3 -m json.tool --sort-keys example.json

[
    {
        "a": "A",
        "b": [
            2,
            4
        ],
        "c": 3.0
    }
]
複製程式碼

參閱


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章