Python 不是强类型语言,开发人员没有给数据定义类型的习惯。这样虽然灵活,但处理复杂业务逻辑的时候却不够方便——缺乏类型检查可能导致很难发现错误,在 IDE 里编码时也没有代码提示。所以开发了这个小工具来解决它。
from typing import List
class Person:
name: str
age: int
class Company:
name: str
revenue: float
employees: List[Person]
之所以选择类变量来定义,是因为它最简洁和直观。相比之下,如果在__init__方法中初始化实例变量,是没有办法获取类型定义( type_hint )的;如果用 @property 注解或者 getter,setter 方法的话,显然就更复杂了。它们都不如直接定义类变量简单优美。不过使用类变量也有缺点:就是它在这里被当成元数据来使用了,如果真的需要定义类级别共享的变量,无法区分。这个问题可以在后面通过开发自定义注解来解决。
from objtyping import objtyping
company1 = objtyping.from_dict_list({
'name': 'Apple',
'revenue': 18.5,
'employees': [{
'name': 'Tom',
'age': 20
}, {
'name': 'Jerry',
'age': 31
}]
}, Company)
此时的 company1 就是完整的 Company 对象了, 可以直接使用 company1.name, company1.employees[0].name 等形式访问里面的属性。
from objtyping import objtyping
dict_list = objtyping.to_dict_list(company1)
此时的 dict_list 对象,就是一大堆 dict 和 list 层级嵌套的原始类型数据
Python 没有 js 那么方便的初始化对象方式,但有这个工具就可以这样写(就是前面基础使用的汇总):
from typing import List
from objtyping import objtyping
class Person:
name: str
age: int
class Company:
name: str
revenue: float
employees: List[Person]
def __str__(self): # 其实一般可能都是这样简单用一下的
return "'{}' has {} employees: {}".format(self.name, len(self.employees), ' and '.join(map(lambda emp: emp.name, self.employees)))
if __name__ == '__main__':
company1 = objtyping.from_dict_list({
'name': 'Apple',
'revenue': 18.5,
'employees': [{
'name': 'Tom',
'age': 20
}, {
'name': 'Jerry',
'age': 31
}]
}, Company)
print(company1)
输出结果:
'Apple' has 2 employees: Tom and Jerry
Python 的常见的序列化需求,包括 json 和 yaml 数据格式,它们都有相对完善的处理库。但同样是不强调类型的缘故,它们处理的对象都是原始的 dict-list 格式。正好可以借助这个工具实现进一步转化。
示例
import json
import sys
from typing import List
from objtyping import objtyping
class X:
x: int
y: str
class A:
q: str
a: str
b: int
c: List[X]
if __name__ == '__main__':
print("\r\n-----json-------")
json_obj = json.loads('{"q":9, "a":"Mark", "b":3, "c":[{"x":15, "y":"male"},{"x":9, "y":"female", "z":13}]}')
typed_obj = objtyping.from_dict_list(json_obj, A)
d_l_obj = objtyping.to_dict_list(typed_obj)
print(json.dumps(d_l_obj))
sys.exit()
输出结果
-----json-------
{"q": "9", "a": "Mark", "b": 3, "c": [{"x": 15, "y": "male"}, {"x": 9, "y": "female", "z": 13}]}
这里需要注意的是:本来属性"q",在最初的 json 结构中,是个数字,但由于类变量定义中是字符串,转换成业务对象以后,它的类型就是字符串了——objtyping 工具,会试图按照类定义,在基础类型之间强制转换。
示例
import sys
from ruamel.yaml import YAML
from typing import List
from objtyping import objtyping
class X:
x: int
y: str
class A:
q: str
a: str
b: int
c: List[X]
if __name__ == '__main__':
print("\r\n-----yaml-------")
yaml = YAML()
yaml_obj = yaml.load('''
q: 9
a: Mark
b: 3
c:
- x: 15
y: male
- x: 9
y: female
z: 13
''')
typed_obj = objtyping.from_dict_list(yaml_obj, A)
d_l_obj = objtyping.to_dict_list(typed_obj)
yaml.dump(d_l_obj, sys.stdout)
sys.exit()
输出结果
-----yaml-------
q: '9'
a: Mark
b: 3
c:
- x: 15
y: male
- x: 9
y: female
z: 13
这里的属性"q"同样被强转了类型。
项目地址:github
1
hsfzxjy 2021-07-29 22:12:05 +08:00 via Android 1
不要用 eval,用 ast.literal_eval,不然可以注入任意代码
|
2
hsfzxjy 2021-07-29 22:17:46 +08:00 via Android 1
另,可以了解一下 dataclasses 模块
|
3
weyou 2021-07-29 22:19:21 +08:00 via Android
支持,感觉业务类的定义比较多余。
之前也写过一个类似的,不仅支持序列化反序列化,还支持几乎 dict/list/tuple 类的所有方法,就是一个 list/dict/tuple 的结合体。且支持对象的修改,比如 append 一个 dict 对象到原来的 list 依然可以使用对象属性的方式来访问嵌套的结构。 |
4
lishunan246 2021-07-29 22:22:44 +08:00 via Android 1
可以了解一下 dacite
|
5
hs0000t 2021-07-29 22:23:18 +08:00 via Android
赞,以前造过类似的轮子,这个看起来更方便一些
|
6
ihawk OP @hsfzxjy 谢谢,我去看看 literal_eval,不过我在 eval 的时候,已经清空了所有环境,只保留最基本的表达式解析,还是比较安全的。
|
7
xiri 2021-07-30 00:22:10 +08:00 1
虽然对这个帖子的内容来说不是什么大问题,但还是得提醒一句“Python 不是强类型语言”这句话错了。
强弱类型针对的是语言是否倾向于对变量类型做隐式转换,Python 是实打实的强类型动态语言。 |
8
ihawk OP @lishunan246 哎,看了一下,dacite 好像还真是做这个的,而且做得挺完整,看来又重新造轮子了。我再仔细研究研究。
|
10
kkbblzq 2021-07-30 01:03:10 +08:00 1
还可以了解一下 Pydantic
|
11
no1xsyzy 2021-07-30 09:31:58 +08:00
@xiri Python 也不完全是强类型语言,魔法特性能够破坏类型保证,应当算是外强中干类型语言(
@ihawk eval 清空环境还不够。你可以从 `()` (空 tuple )里构建出任意字节码并封装成函数执行。 (via <https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html>) s = "([c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('ls'))" |
12
ihawk OP @weyou 理解你的意思了,目标不太一样,你的项目是想尽量方便地把 dict 转换成对象,实现得还挺强大的。不过我这里是假设已经有业务对象了,就是想按指定的类型反序列化。
|
13
ihawk OP @xiri 嗯,我是这样理解“强类型”的:就是一个变量或属性,声明的时候是什么类型,赋值的时候,就必须是这个类型。从这个意思上,Python 应该不属于强类型。Python 的数据类型设计思路不也称为“duck typing”么:只要看起来像是这个类型,就可以用了,至于它本来如何声明的,不重要。
所以我这个小工具(包括 dacite ),可能不太 pythonic,不过语言都在互相借鉴,PEP 这两年也一直在加强类型声明。 |
15
ihawk OP @AX5N 不是吧,哪有这么武断的,从强类型到弱类型,是一个渐变的过程。从维基百科 [https://zh.wikipedia.org/wiki/%E5%BC%B7%E5%BC%B1%E5%9E%8B%E5%88%A5] 列出的一系列“强类型”要素来看,以下几条 Python 肯定是不符合的:
* 类型是与变量相连系的名称(通常是在声明时),而不是值(通常是在初始化时) * 拒绝(在要么编译时间要么运行时间)尝试忽视资料类型的操作或函数调用 * 禁止类型转换。某个类型的值,不论是不是以显式或隐式的方式,都不可转换为另一个类型。 显然它的类型系统不是那么“强”。 当然, 这不是本帖的重点,而且 Benjamin 也说了:“这些术语的用法不尽相同,所以也就近乎无用”。 |
16
cyrivlclth 2021-08-04 15:29:35 +08:00
可以用 dataclass 吧?
|