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

2021-07-29 21:55:31 +08:00
 ihawk

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

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 格式。正好可以借助这个工具实现进一步转化。

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

1946 次点击
所在节点    分享创造
16 条回复
hsfzxjy
2021-07-29 22:12:05 +08:00
不要用 eval,用 ast.literal_eval,不然可以注入任意代码
hsfzxjy
2021-07-29 22:17:46 +08:00
另,可以了解一下 dataclasses 模块
weyou
2021-07-29 22:19:21 +08:00
支持,感觉业务类的定义比较多余。

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

所以我这个小工具(包括 dacite ),可能不太 pythonic,不过语言都在互相借鉴,PEP 这两年也一直在加强类型声明。
AX5N
2021-07-31 16:06:42 +08:00
@ihawk 人家说的是正确的,你就别乱理解了
ihawk
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
2021-08-04 15:29:35 +08:00
可以用 dataclass 吧?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/792565

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX