如何實現(xiàn)一個優(yōu)雅的Python的Json序列化庫

在Python的世界里,將一個對象以json格式進(jìn)行序列化或反序列化一直是一個問題。Python標(biāo)準(zhǔn)庫里面提供了json序列化的工具,我們可以簡單的用json.dumps來將一個對象序列化。但是這種序列化僅支持python內(nèi)置的基本類型。

Python

在Python的世界里,將一個對象以json格式進(jìn)行序列化或反序列化一直是一個問題。Python標(biāo)準(zhǔn)庫里面提供了json序列化的工具,我們可以簡單的用json.dumps來將一個對象序列化。但是這種序列化僅支持python內(nèi)置的基本類型,對于自定義的類,我們將得到Object of type A is not JSON serializable的錯誤。

有很多種方法可以用來支持這種序列化,這里有一個很長的關(guān)于這個問題的討論??偨Y(jié)起來,基本上有兩種還不錯的思路:

  1. 利用標(biāo)準(zhǔn)庫的接口:從python標(biāo)準(zhǔn)json庫中的JSONDecoder繼承,然后自定義實現(xiàn)一個default方法用來自定義序列化過程
  2. 利用第三方庫實現(xiàn):如jsonpickle jsonweb json-tricks

利用標(biāo)準(zhǔn)庫的接口的問題在于,我們需要對每一個自定義類都實現(xiàn)一個JSONDecoder.default接口,難以實現(xiàn)代碼復(fù)用。

利用第三方庫,對我們的代碼倒是沒有任何侵入性,特別是jsonpickle,由于它是基于pickle標(biāo)準(zhǔn)序列化庫實現(xiàn),可以實現(xiàn)像pickle一樣序列化任何對象,一行代碼都不需要修改。

但是我們觀察這類第三方庫的輸出的時候,會發(fā)現(xiàn)所有的這些類庫都會在輸出的json中增加一個特殊的標(biāo)明對象類型的屬性。這是為什么呢?Python是一門動態(tài)類型的語言,我們無法在對象還沒有開始構(gòu)建的時候知道對象的某一屬性的類型信息,為了對反序列化提供支持,看起來確實是不得不這么做。

有人可能覺得這也無可厚非,似乎不影響使用。但是在跨語言通信的時候,這就成為了一個比較麻煩的問題。比如我們有一個Python實現(xiàn)的API,客戶端發(fā)送了一個json請求過來,我們想在統(tǒng)一的一個地方將json反序列化為我們Python代碼的對象。由于客戶端不知道服務(wù)器端的類型信息,json請求里面就沒法加入這樣的類型信息,這也就導(dǎo)致這樣的類庫在反序列化的時候遇到問題。

能不能有一個相對完美的實現(xiàn)呢?先看一下我們理想的json序列化庫的需求:

  1. 我們希望能簡單的序列化任意自定義對象,只添加一行代碼,或者不加入任何代碼
  2. 我們希望序列化的結(jié)果不加入任何非預(yù)期的屬性
  3. 我們希望能按照指定的類型進(jìn)行反序列化,能自動處理嵌套的自定義類,只需要自定義類提供非常簡單的支持,或者不需要提供任何支持
  4. 我們希望反序列化的時候能很好的處理屬性不存在的情況,以便在我們加入某一屬性的時候,可以設(shè)置默認(rèn)值,使得舊版本的序列化結(jié)果可以正確的反序列化出來

如果有一個json庫能支持上面的四點,那就基本是比較好用的庫了。下面我們來嘗試實現(xiàn)一下這個類庫。

對于我們想要實現(xiàn)的幾個需求,我們可以建立下面這樣的測試來表達(dá)我們所期望的庫的API設(shè)計:

class A(JsonSerializable):

    def __init__(self, a, b):
        super().__init__()
        self.a = a
        self.b = b if b is not None else B(0)

    @property
    def id(self):
        return self.a

    def _deserialize_prop(self, name, deserialized):
        if name == 'b':
            self.b = B.deserialize(deserialized)
            return
        super()._deserialize_prop(name, deserialized)


class B(JsonSerializable):

    def __init__(self, b):
        super().__init__()
        self.b = b


class JsonSerializableTest(unittest.TestCase):

    def test_model_should_serialize_correctly(self):
        self.assertEqual(json.dumps({'a': 1, 'b': {'b': 2}}), A(1, B(2)).serialize())

    def test_model_should_deserialize_correctly(self):
        a = A.deserialize(json.dumps({'a': 1, 'b': {'b': 2}}))
        self.assertEqual(1, a.a)
        self.assertEqual(2, a.b.b)

    def test_model_should_deserialize_with_default_value_correctly(self):
        a = A.deserialize(json.dumps({'a': 1}))
        self.assertEqual(1, a.a)
        self.assertEqual(0, a.b.b)

這里我們希望通過繼承的方式來添加支持,這將在反序列化的時候提供一個好處。因為有了它我們就可以直接使用A.deserialize方法來反序列化,而不需要提供任何其他的反序列化函數(shù)參數(shù),比如這樣json.deserialize(serialized_str, A)。

同時為了驗證我們的框架不會將@property屬性序列化或者反序列化,我們特意在類A中添加了這樣一個屬性。

由于在反序列化的時候,框架是無法知道某一個對象屬性的類型信息,比如測試中的A.b,為了能正確的反序列化,我們需要提供一點簡單的支持,這里我們在類A中覆蓋實現(xiàn)了一個父類的方法_deserialize_prop對屬性b的反序列化提供支持。

當(dāng)我們要反序列化一個之前版本的序列化結(jié)果時,我們希望能正確的反序列化并使用我們提供的默認(rèn)值作為最終的反序列化值。這在屬性A.b的測試中得到了體現(xiàn)。

(上面的測試有很多邊界的情況、支持的變量類型并沒有覆蓋,此測試只是作為示例使用。)

如果能有一個類可以讓上面的測試通過,相信那個類就是我們所需要的類了。這樣的類可以實現(xiàn)為如下:

def is_normal_prop(obj, key):
    is_prop = isinstance(getattr(type(obj), key, None), property)
    is_func_attr = callable(getattr(obj, key))
    is_private_attr = key.startswith('__')
    return not (is_func_attr or is_prop or is_private_attr)


def is_basic_type(value):
    return value is None or type(value) in [int, float, str, bool]


class JsonSerializable:

    def _serialize_prop(self, name):
        return getattr(self, name)

    def _as_dict(self):
        props = {}
        for key in dir(self):
            if not is_normal_prop(self, key):
                continue
            value = self._serialize_prop(key)
            if not (is_basic_type(value) or isinstance(value, JsonSerializable)):
                raise Exception('unknown value to serialize to dict: key={}, value={}'.format(key, value))
            props[key] = value if is_basic_type(value) else value._as_dict()
        return props

    def serialize(self):
        return json.dumps(self._as_dict(), ensure_ascii=False)

    def _deserialize_prop(self, name, deserialized):
        setattr(self, name, deserialized)

    @classmethod
    def deserialize(cls, json_encoded):
        if json_encoded is None:
            return None

        args = inspect.getfullargspec(cls)
        args_without_self = args.args[1:]
        obj = cls(*([None] * len(args_without_self)))

        data = json.loads(json_encoded, encoding='utf8') if type(json_encoded) is str else json_encoded
        for key in dir(obj):
            if not is_normal_prop(obj, key):
                continue
            if key in data:
                obj._deserialize_prop(key, data[key])
        return obj

在實現(xiàn)時,我們利用了Python的內(nèi)省機制,這樣就可以自動的識別對象的屬性及運行時類型了。當(dāng)然對于這個簡單的類還有很多待支持的功能,使用上也有很多限制,比如:

  1. 當(dāng)某一屬性為自定義類的類型的時候,需要子類覆蓋實現(xiàn)_deserialize_prop方法為反序列化過程提供支持
  2. 當(dāng)某一屬性為由自定義類構(gòu)成的一個list tuple dict復(fù)雜對象時,需要子類覆蓋實現(xiàn)_deserialize_prop方法為反序列化過程提供支持
  3. 簡單屬性必須為python內(nèi)置的基礎(chǔ)類型,比如如果某一屬性的類型為numpy.float64,序列化反序列化將不能正常工作

雖然有上述限制,但是這正好要求我們在做模型設(shè)計的時候保持克制,不要將某一個對象設(shè)計得過于復(fù)雜。比如如果有屬性為dict類型,我們可以將這個dict抽象為另一個自定義類型,然后用類型嵌套的方式來實現(xiàn)。

到這里這個基類就差不多可以支撐我們?nèi)粘5拈_發(fā)需要了。當(dāng)然對于這個簡單的實現(xiàn)還有可能有其他的需求或者問題,大家如有發(fā)現(xiàn),歡迎留言交流。

文/ThoughtWorks廖光明

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容