把JSON資料格式轉換為Python的類物件

昀溪發表於2019-06-04

JOSN字串轉換為自定義類例項物件

有時候我們有這種需求就是把一個JSON字串轉換為一個具體的Python類的例項,比如你接收到這樣一個JSON字串如下:

{"Name": "Tom", "Sex": "Male", "BloodType": "A", "Hobbies": ["籃球", "足球"]}

我需要把這個轉換為具體的一個Person類的例項,透過物件的方式來進行操作。在Java中有很多實現比如Gson或者FastJosn。如下程式碼所示(這裡不是全部程式碼,值標識最主要的部分):

import com.alibaba.fastjson.JSONObject;
import com.example.demo.entity.Product;


String a = "{\"gmtCreate\":1559009853000,\"dataFormat\":1,\"deviceCount\":1,\"nodeType\":0,\"productKey\":\"a1U85pSQrAz\",\"productName\":\"溫度計\"}";

//JSON字串反序列化為一個Product物件
Product product = JSONObject.parseObject(a, Product.class);

上述這種需求一般發生在前段傳遞過來JSON字串或者其他系統進行RPC通訊的時候也傳送過來JSON字串,作為接收端需要反序列化成物件來進行處理,而且Fastjson裡還有一個JSONArray.parseArray方法可以轉換為物件列表。可是在Python沒有像Java中這麼方便的東西。當然在Django的RESTframework框架中有這種進行序列化和反序列化的功能可以使用。這裡只是對一個問題的思考,如果遇到這種情況自己怎麼解決。

從網上論壇中也看到過一些,不過很多都是效果有但是使用起來麻煩,所以我這裡也來說一下我的思路。

方式1:透過josn.loads來實現

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import json


class Person:
    def __init__(self, data=None):
        self._name = "1"
        self._sex = ""
        self._blood_type = "O"
        self._hobbies = []

        self._date_of_birth = "1900/1/1"

        if data:
            self.__dict__ = data

    # 透過屬性的方式來獲取和設定例項變數的值,如果不這樣那麼就只能透過set或者get方法來做
    @property
    def date_of_brith(self):
        return self._date_of_birth

    @date_of_brith.setter
    def date_of_brith(self, date_of_brith):
        self._date_of_birth = date_of_brith


def main():
    try:
        str1 = '{"name": "Tom", "sex": "male", "blood_type": "A", "hobbies": ["籃球", "足球"]}'
        person1 = json.loads(str1, object_hook=Person)
        print(isinstance(person1, Person))
        # 這裡你會發現沒有date_of_brith這個內容
        print(person1.__dict__)
        # 獲取date_of_brith屬性值報錯,因為JSON字串不包含這個鍵,但是類中的例項變數有這個,正常來講你應該可以獲取預設值,但是由於
        # 替換了__dict__,所以就沒有了,因為__dict__原本就是例項變數的字典形式,你替換了自然也就找不到原來的了。
        # print(person.date_of_brith)

        # 下面我們透過正常的方式例項化一個物件
        person2 = Person()
        print(person2.__dict__)
        print(person2.date_of_brith)

    except Exception as err:
        print(err)

if __name__ == "__main__":
    try:
        main()
    finally:
        sys.exit()

object_hook的含義是,預設json.loads()返回的是dict,你可以使用object_hook來讓其返回其他型別的值,它這裡實現的原理就是把你傳遞進來的JSON字串傳遞給了object_hook指定的方法或者類(如果是類的話則會執行__init__方法,其實就是例項化),這時候在類的__init__方法中我們透過賦值給self.dict,其實這就等於對Person類的例項變數做了替換,除非你的JSON字串的鍵和例項變數的名稱以及數量一致否則你無法透過你在類裡定義的例項變數名稱獲取透過JSON字串傳遞進去的值。如下圖:

所以透過上面可以看出來,這個過程不是為例項變數賦值的過程而是一個替換的過程,Python是動態語言這一點和JAVA不同。如果你在程式中用單下劃線標識變數為私有(只是規範而不是真正的私有)那麼你傳遞的JSON字串的鍵也需要有下劃線,這樣你透過例項的方法才能獲取。既然額外增加下劃線不太現實,那麼有沒有其他辦法呢?看方式2

方式2:透過反射機制來實現

先看一下類的定義

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class Person:
    def __init__(self):
        self._name = "1"
        self._sex = ""
        self._blood_type = "O"
        self._hobbies = []
        self._date_of_birth = "1900/1/1"

    def __str__(self):
        """
        輸出例項的類名字,而不是一個地址
        :return: 該例項的類名字
        """
        return self.__class__.__name__

    # 當一個方法加上這個裝飾器之後,hasattr()中的屬性要寫成這個方法的名稱,而不是例項變數的名稱。
    # 如果不加這個裝飾器,那麼hasattr()中的屬性名稱要和例項變數的名稱保持一致
    @property
    def Name(self):
        return self._name

    @Name.setter
    def Name(self, name):
        self._name = name

    @property
    def Sex(self):
        return self._sex

    @Sex.setter
    def Sex(self, sex):
        self._sex = sex

    @property
    def BloodType(self):
        return self._blood_type

    @BloodType.setter
    def BloodType(self, blood_type):
        self._blood_type = blood_type

    @property
    def Hobbies(self):
        return self._hobbies

    @Hobbies.setter
    def Hobbies(self, hobbies):
        self._hobbies = hobbies

    @property
    def date_of_brith(self):
        return self._date_of_birth

    @date_of_brith.setter
    def date_of_brith(self, date_of_brith):
        self._date_of_birth = date_of_brith

下面看看轉換的方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import json
import importlib


def get_instance(str_stream, class_full_path=None):
    """
    :param str_stream: json的字串形式 '{"Name": "Tom", "Sex": "Male", "BloodType": "A"}'
    :param class_full_path: package.module.class
    :return:
    """
    try:
        json_obj = json.loads(str_stream)
    except Exception as err:
        print("輸入的字串不符合JSON格式,請檢查。")
        return None

    if class_full_path is None:
        return json_obj
    else:
        try:
            # 獲取模組路徑
            module_path = ".".join(class_full_path.split(".")[0:-1])
            # 獲取類名稱
            class_name = class_full_path.split(".")[-1]
            # 透過模組名載入模組
            CC = importlib.import_module(module_path)

            # 判斷是否有class_name所代表的屬性
            if hasattr(CC, class_name):
                # 獲取模組中屬性
                temp_obj = getattr(CC, class_name)
                # 例項化物件
                obj = temp_obj()

                for key in json_obj.keys():
                    obj.__setattr__(key,  json_obj[key])

                return obj
            else:
                pass
        except Exception as err:
            print(err)
            return None


def main():
    try:
        str1 = '{"Name": "Tom", "Sex": "Male", "BloodType": "A", "Hobbies": ["籃球", "足球"]}'
        person1 = get_instance(str1, class_full_path="AAA.Classes.Person")
        # 檢視型別
        print(type(person1))
        # 檢視屬性
        print(person1.__dict__)
        # 檢視指定屬性
        print(person1.Name)

    except Exception as err:
        print(err)

if __name__ == "__main__":
    try:
        main()
    finally:
        sys.exit()


__import__() 有2個引數,第一個是類,第二個是fromlist,如果不寫fromlist,則按照下面的寫法會只匯入AAA包,如果fromlist有值則會匯入AAA下面的Classes模組cc = __import__("AAA.Classes", fromlist=True)不寫fromlist 相當於 import AAA ,如果寫了就相當於是from AAA import Classes程式設計時如果使用動態載入建議使用importlib.import_module(),而不是__import__()

下面看一下效果:

可以看到,這樣操作之後就是給例項變數賦值而不是像之前那樣的替換,而且保留了類中例項變數的私有規範。不過需要說明的是JSON字串中的鍵名稱要和類裡面定義的屬性名稱一樣,也就是鍵名稱要和類中@property裝飾的方法同名。我們也可以看到這種使用方式也有預設JSONObject.parseObject的意思。

不過這只是一個簡單的實現,只能透過單一JSON字串生成物件不能生成物件列表。當然有興趣的朋友可以自己根據這個思路進行擴充套件。

另外既然無論是loads還是我自己的方法搜需要保證JSON字串的鍵和變數名稱一致大家就不要糾結於名稱一致的問題,但是我的方法做到了保持例項變數命名、操作例項屬性時候的規範,同時對類也沒有過多的入侵性而不像loads方法中還需要在類的init方法裡面增加不必要的內容。在我這個方法中如果能實現忽略大小寫通用性就更好了。歡迎大家來提供思路。

相關文章