一直觉得 Python 里初始化对象不方便,反序列化配置文件就更不方便,写了一个超小项目,把 dict 数据(嵌套了 list 也可以), 变成预定义好的数据类型的实例对象 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
ihawk
V2EX    分享创造

一直觉得 Python 里初始化对象不方便,反序列化配置文件就更不方便,写了一个超小项目,把 dict 数据(嵌套了 list 也可以), 变成预定义好的数据类型的实例对象

  •  
  •   ihawk 2021-07-29 21:55:31 +08:00 2630 次点击
    这是一个创建于 1559 天前的主题,其中的信息可能已经有所发展或是发生改变。

    objtyping 带类型定义的对象转换器

    由来

    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 方法的话,显然就更复杂了。它们都不如直接定义类变量简单优美。不过使用类变量也有缺点:就是它在这里被当成元数据来使用了,如果真的需要定义类级别共享的变量,无法区分。这个问题可以在后面通过开发自定义注解来解决。

    • 下一步就可以把符合这个类定义结构的 dict-list 嵌套数据,转化为该类实例对象了:
    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 等形式访问里面的属性。

    • 当然也可以把业务对象再转回 dict-list 嵌套的形式
    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 格式。正好可以借助这个工具实现进一步转化。

    json

    示例

    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 工具,会试图按照类定义,在基础类型之间强制转换。

    yaml

    示例

    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

    16 条回复    2021-08-04 15:29:35 +08:00
    hsfzxjy
        1
    hsfzxjy  
       2021-07-29 22:12:05 +08:00 via Android   1
    不要用 eval,用 ast.literal_eval,不然可以注入任意代码
    hsfzxjy
        2
    hsfzxjy  
       2021-07-29 22:17:46 +08:00 via Android   1
    另,可以了解一下 dataclasses 模块
    weyou
        3
    weyou  
       2021-07-29 22:19:21 +08:00 via Android
    支持,感觉业务类的定义比较多余。

    之前也写过一个类似的,不仅支持序列化反序列化,还支持几乎 dict/list/tuple 类的所有方法,就是一个 list/dict/tuple 的结合体。且支持对象的修改,比如 append 一个 dict 对象到原来的 list 依然可以使用对象属性的方式来访问嵌套的结构。
    lishunan246
        4
    lishunan246  
       2021-07-29 22:22:44 +08:00 via Android   1
    可以了解一下 dacite
    hs0000t
        5
    hs0000t  
       2021-07-29 22:23:18 +08:00 via Android
    赞,以前造过类似的轮子,这个看起来更方便一些
    ihawk
        6
    ihawk  
    OP
       2021-07-30 00:09:50 +08:00
    @hsfzxjy 谢谢,我去看看 literal_eval,不过我在 eval 的时候,已经清空了所有环境,只保留最基本的表达式解析,还是比较安全的。
    xiri
        7
    xiri  
       2021-07-30 00:22:10 +08:00   1
    虽然对这个帖子的内容来说不是什么大问题,但还是得提醒一句“Python 不是强类型语言”这句话错了。
    强弱类型针对的是语言是否倾向于对变量类型做隐式转换,Python 是实打实的强类型动态语言。
    ihawk
        8
    ihawk  
    OP
       2021-07-30 00:25:46 +08:00
    @lishunan246 哎,看了一下,dacite 好像还真是做这个的,而且做得挺完整,看来又重新造轮子了。我再仔细研究研究。
    ihawk
        9
    ihawk  
    OP
       2021-07-30 00:28:49 +08:00
    @hsfzxjy 好, 我也看看 dataclasses 装饰器,dacite 好像是要求必须要用这个装饰器的
    td width="auto" valign="top" align="left">
        10
    kkbblzq  
       2021-07-30 01:03:10 +08:00   1
    还可以了解一下 Pydantic
    kkbblzq
    no1xsyzy
        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'))"
    ihawk
        12
    ihawk  
    OP
       2021-07-30 10:43:30 +08:00
    @weyou 理解你的意思了,目标不太一样,你的项目是想尽量方便地把 dict 转换成对象,实现得还挺强大的。不过我这里是假设已经有业务对象了,就是想按指定的类型反序列化。
    ihawk
        13
    ihawk  
    OP
       2021-07-31 11:36:27 +08:00
    @xiri 嗯,我是这样理解“强类型”的:就是一个变量或属性,声明的时候是什么类型,赋值的时候,就必须是这个类型。从这个意思上,Python 应该不属于强类型。Python 的数据类型设计思路不也称为“duck typing”么:只要看起来像是这个类型,就可以用了,至于它本来如何声明的,不重要。

    所以我这个小工具(包括 dacite ),可能不太 pythonic,不过语言都在互相借鉴,PEP 这两年也一直在加强类型声明。
    AX5N
        14
    AX5N  
       2021-07-31 16:06:42 +08:00
    @ihawk 人家说的是正确的,你就别乱理解了
    ihawk
        15
    ihawk  
    OP
       2021-08-01 21:25:01 +08:00
    @AX5N 不是吧,哪有这么武断的,从强类型到弱类型,是一个渐变的过程。从维基百科 [https://zh.wikipedia.org/wiki/%E5%BC%B7%E5%BC%B1%E5%9E%8B%E5%88%A5] 列出的一系列“强类型”要素来看,以下几条 Python 肯定是不符合的:

    * 类型是与变量相连系的名称(通常是在声明时),而不是值(通常是在初始化时)
    * 拒绝(在要么编译时间要么运行时间)尝试忽视资料类型的操作或函数调用
    * 禁止类型转换。某个类型的值,不论是不是以显式或隐式的方式,都不可转换为另一个类型。

    显然它的类型系统不是那么“强”。

    当然, 这不是本帖的重点,而且 Benjamin 也说了:“这些术语的用法不尽相同,所以也就近乎无用”。
    cyrivlclth
        16
    cyrivlclth  
       2021-08-04 15:29:35 +08:00
    可以用 dataclass 吧?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5093 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 09:22 PVG 17:22 LAX 01:22 JFK 04:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86