Python實現JSON反序列化類物件

挨踢的懶貓發表於2018-01-30

我們的網路協議一般是把資料轉換成JSON之後再傳輸。之前在Java裡面,實現序列化和反序列化,不管是jackson,還是fastjson都非常的簡單。現在有專案需要用Python來開發,很自然的希望這樣的便利也能在Python中體現。

但是在網上看了一些教程,講反序列化的時候,基本都是轉換為dict或者array。這種程式設計方式我從情感上是無法接受的。難道是這些JSON庫都不支援反序列化為類物件?我馬上打消了這個念頭,Python這樣強大的指令碼語言,不可能沒有完善的JSON庫。

於是我就研究了一下原生的json,以及第三方的demjsonsimplejson

一、原生json

我仔細研究了原生jsonloads方法的定義

def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)
複製程式碼

這裡面的object_hookobject_pairs_hook引數引起了我的注意,我重點說一下object_hook

官方文件的說明如下:

object_hook is an optional function that will be called with the result of any object literal decoded (a dict). The return value of object_hook will be used instead of the dict. This feature can be used to implement custom decoders (e.g. JSON-RPC class hinting).

這個object_hook根據文件的解釋就是一個自定義解碼函式,入引數標準反序列化後的dict,我們可以根據自己的規則轉換輸出為想要的格式。

我又去搜了一下object_hook,大家對於這個東西的處理方式基本就是用一個靜態方法把dict轉換成物件。

我們的資料結構是這樣的

{"status":1,"info":"釋出成功","data":{"id":"52","feed_id":"70"}}
複製程式碼

於是我就寫了這樣的程式碼:

class Response:

    def __init__(self, status, info, data) -> None:
        super().__init__()
        self.status = status
        self.info = info
        self.data = data

    @staticmethod
    def object_hook(d):
        return Response(d['status'], d['info'], d['data'])
 ...
resp = json.loads(body, object_hook=Response.object_hook)
複製程式碼

一開始呢,確實沒有問題,雖然用起來沒有java的json庫辣麼方便,但總歸實現了需求。

好景不長,我測試的第一個介面返回的資料中,data是欄位一個字串,反序列化正常。可是後來當介面返回的結構中data欄位是一個dict結構的時候,object_hook的入參居然變成了data欄位轉換之後的dict({"id":"52","feed_id":"70"}),而不是完整的資料。

這些懵逼了,上網搜尋了一圈沒有結論。好吧,我最後又老老實實回到官方文件,read the fucking official document

不看不知道,一看嚇一跳,官方文件用了一種巧妙的方式實現了上面的需求。

>>> class JSONObject:
...     def __init__(self, d):
...         self.__dict__ = d
...
>>>
>>> data = json.loads(s, object_hook=JSONObject)
>>> data.name
'ACME'
>>> data.shares
50
>>> data.price
490.1
>>>
複製程式碼

我服了,把json解析之後的dict直接賦值給物件的屬性dict,然後就可以隨心所欲的使用屬性了,真心方便,動態語言就是好。

以上是官方的json庫實現方案,那另外兩個知名的第三方庫呢?

二、demjson

demjson也支援hook。有兩種配置的方式:decode函式配置和set_hook函式配置

1. decode

def decode( txt, encoding=None, **kwargs )
複製程式碼

decode函式可以指定很多引數,其中就包括hook函式。hook函式的指定是使用鍵值對的方式,鍵是hook函式的名稱,值是hook函式。

demjson是通過名字來管理hook函式的,所以hookname不是隨便指定的,必須是內建的幾種hook函式的名稱。

  • decode_number
  • decode_float
  • decode_object
  • decode_array
  • decode_string
  • encode_value
  • encode_dict
  • encode_dict_key
  • encode_sequence
  • encode_bytes
  • encode_default
demjson.decode(body, encode='utf-8',decode_obbject=Reponse.object_hook)
複製程式碼

結果並沒有讓我很開森,依然是無法處理巢狀結構。 日誌中顯示如下內容:

2018-01-30 16:01:17,137 poster.py post_all 73 INFO    : {"status":1,"info":"\u53d1\u5e03\u6210\u529f","data":{"id":"54","feed_id":"72"}}
2018-01-30 16:01:17,138 response.py object_hook 13 INFO    : {'id': '54', 'feed_id': '72'}
2018-01-30 16:01:17,138 response.py object_hook 13 INFO    : {'status': 1, 'info': '釋出成功', 'data': demjson.undefined}
複製程式碼

很奇怪的是object_hook 函式被呼叫了兩次,第一次是data欄位的內容,第二是全部的內容,但是data欄位沒有解析出來。 非常奇怪,百思不得其解!!!

2. set_hook

set_hook函式跟上面的decode函式不一樣,它是JSON類的成員函式,而decode函式是個靜態函式。

def set_hook(self, hookname, function)
複製程式碼

吸取之前的教訓,這次我仔細閱讀了demjson的文件,還真發現點東西。

Netsted values. When decoding JSON that has nested objects or arrays, the decoding hooks will be called once for every corresponding value, even if nested. Generally the decoding hooks will be called from the inner-most value outward, and then left to right.

這裡重點說到巢狀的問題,出現巢狀的時候,每個對應的型別都會呼叫hook函式一次,而且是從最內層,從左往右。好吧,之前出現的問題全部明白了,原來都是這個規則惹的禍,但是為什麼這樣設計我暫時還是不明白。

set_hook的使用方式

    j = demjson.JSON()
    j.set_hook( 'decode_array', my_sort_array )
    j.decode(body, encode='utf-8')
複製程式碼

三、simplejson

前面說了那麼多,simplejson的方式就沒什麼可說的,跟官方的jsonhook方式一致。

總結

雖然我的需求是滿足了,但是還是有一個大大的問號留在我心中,為什麼是這樣設計,網上沒有找到合適的答案,剩下的需要研究原始碼分析了。

相關文章