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 中这么方便的东西.
从网上论坛中也看到过一些, 不过很多都是效果有但是使用起来麻烦, 所以我这里也来说一下我的思路.
方式 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 方法里面增加不必要的内容. 在我这个方法中如果能实现忽略大小写通用性就更好了. 欢迎大家来提供思路.
来源: https://www.cnblogs.com/rexcheny/p/10973543.html