V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
ihawk
V2EX  ›  分享创造

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

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

    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 好像是要求必须要用这个装饰器的
    kkbblzq
        10
    kkbblzq  
       2021-07-30 01:03:10 +08:00   ❤️ 1
    还可以了解一下 Pydantic
    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   ·   实用小工具   ·   4153 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 05:25 · PVG 13:25 · LAX 21:25 · JFK 00:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.