1 什么是 ex-dataclass?
- 它是一款用于降低维护成本和提高开发效率的数据管理对象,它是基于
dataclass
开发的。;-
简单看看它是如何降低维护成本,只要你会简单的类语法即可实现;
- 传统写法
# 传统写法,各种复杂的字段,甚至可能会有嵌套字段 # 给定一段数据 data = { "user_id": 1, "username": "zhangsan", "age": 18, "gender": "male", "details": { "address": "xxxx", "phone": "1234567890", }, } # 这样的取值方式,特别难记忆;而且再参数传递过程里经过一系列动作后, # 使得数据模型难以准确得知,导致维护和开发成本大大提高; user_id = data['user_id'] age = data['age] phone = data['details']['phone']
- ex-dataclass 数据对象
from ex_dataclass import ex_dataclass, filed # 预先定义好数据模型,后面找起来就有根据 @ex_dataclass class UserDetail: # 这种冒号后面带类型是python3.5之后就有的了 # field 是为这个字段提供默认值,支持 default=0 给定默认值,default_factory=int 给定一个可调用的类型 phone: str = field(default_factory=str) address: str = field(default_factory=str) @ex_dataclass class User: user_id: int = field(default_factory=int) username: str = field(default_factory=str) age: int = field(default=18) gender: str = field(default="male") details: UserDetail = field(default_factory=UserDetail) # 给定一段数据 data = { "user_id": 1, "username": "zhangsan", "age": 18, "gender": "male", "details": { "address": "xxxx", "phone": "1234567890", }, } # 将数据直接转成容易管理的数据对象,这样就算经历多次的传递也不会乱套 u = User(**data) # 当然也可这样去填充,如: # u = User(user_id=1, age=20 details=UserDetail(phone="xxx", address="xxx")) # 下面的所有写法,IDE 都会进行提示非常的友好 print(u.user_id) print(u.age) print(u.details.phone)
-
1.1 背景
1.1.1 诞生
从上面的简单例子可以看出 ex-dataclass 的出发点就是为了解决 python 中数据模型非常难以维护的问题,这个问题在稍微大点的项目中都让人非常的头疼。其实在早前官方推出过 dataclass 和 typing 两个内置库来解决这个难题,但效果不明显,甚至很多人都完全不知道。最重要的是 dataclass 难以满足日常需求。 如果你提到这样写 python 会把代码写复杂了,但其实你明确了数据结构后开发效率只会翻倍提升。
自我们在生产环境上开始使用ex-dataclass,当代码在review 或者排障的时候再也没有去担忧数据结构变动或者数据模型不明确的问题。于是我们便有了重构并开源该项目的想法,让这个项目变成开发 Python 时的日常使用库。
下面看看 ex-dataclass 对数据模型转换为数据对象的能力。
2 使用文档
2.1 简介
ex-dataclass 是通过注解类型的协助把字典数据转换成数据对象;如 str,int,typing.List 等;
更多的介绍可以到仓库去看看,在 example 中有大量的样例;[源码地址](https://github.com/Shadow-linux/ex_dataclass
支持的类型注解
- int
- str
- float
- bool
- dict
- list
- typing.Dict
- typing.List
- typing.Union
- typing.Type
- ex_dataclass cover的类型
- 功能
- 支持
ex_dataclass
类型继承的正反解析; - 支持
typing.List
中ex_dataclass
类型正反解析; - 支持
typing.List
嵌套正反解析,如:{a: [[{a:1, b:2}, {a:3, b:4}]]}
; - 支持
typing.Union
和typing.Type
特殊容器类型注解的多态行为,精确匹配字段存在最多ex_dataclass
类; - 支持反向解析下存在冗余字段,并且抛弃该字段
- 支持typing.Union 和 typing.Type 特殊容器类型相互嵌套场景 ;
- 支持纯净的class类型的正反转换解析;
- 支持
ex_dataccass
字段检测校验,通过类型注解获取类后进行值的校验;
2.2 快速开始
- 安装
pip install ex-dataclass
- 因为 ex-dataclass 与 python 的类型注解、typing模块息息相关,可以通过该文章快速入门 typing模块介绍 或 官方文档
2.2.0 示例1(转化)
import typing
from ex_dataclass import ex_dataclass, field, asdict, EXpack
@ex_dataclass
class User:
name: str = field(default_factory=str)
# default: 给定一个默认值, 再不给定的时候默认会使用
age: int = field(default=18)
# 赋值方法-1
u1 = User(name="u1", age=20)
u1.age
u1.name
# 赋值方法-2
data = {"name": "u2", "age": 18}
u2 = User(**data)
u2.name
# 冗余数据会被丢弃
data = {"name": "u2", "age": 18, "ignore": True}
u3 = User(**data)
print(u3)
# :return: User(name='u2', age=18)
# 对象数据转dict
data_dict = asdict(u3)
print(data_dict)
2.2.1 示例2(日常使用频率高)
留意注释
import typing
from ex_dataclass import ex_dataclass, asdict, field, EXpack
# 给定一段data
data = {
"teams": [
{
"team_name": "Team-A",
"users" : [
{
"name": "zhangsan",
"age" : 18,
},
{
"name": "lisi",
"age" : 18,
}
]
},
{
"team_name": "Team-B",
"users" : [
{
"name": "jack",
"age" : 18,
},
{
"name": "rose",
"age" : 18,
}
]
}
]
}
@ex_dataclass
class User:
# default_factory: 需要给一个类(可callable)
name: str = field(default_factory=str)
# default: 给定一个默认值
age: int = field(default=0)
@ex_dataclass
class Team:
team_name: str = field(default_factory=str)
# 没有值时,我们设置一个list给users字段
users: typing.List[User] = field(default_factory=list)
@ex_dataclass
class AllTeam:
teams: typing.List[Team] = field(default_factory=list)
# 看看TeamUser 接受参数或字典
all_team = AllTeam(**data)
# 可以看到运行结果,所有类型都被转换成对象,对象在python中是非常的友好可以进行全方位自动补全,并且方便维护;
print(all_team)
# AllTeam(teams=[Team(team_name='Team-A', users=[User(name='', age=18), User(name='', age=18)]), Team(team_name='Team-B', users=[User(name='', age=18), User(name='', age=18)])])
print(all_team.teams)
# [Team(team_name='Team-A', users=[User(name='', age=18), User(name='', age=18)]), Team(team_name='Team-B', users=[User(name='', age=18), User(name='', age=18)])]
print(all_team.teams[0].team_name)
print(all_team.teams[0].users)
# Team-A
# [User(name='', age=18), User(name='', age=18)]
print(all_team.teams[0].users[0].name)
# zhangsan
# 重新转回字典
print(asdict(all_team))
# {'teams': [{'team_name': 'Team-A', 'users': [{'name': 'zhangsan', 'age': 18}, {'name': 'lisi', 'age': 18}]}, {'team_name': 'Team-B', 'users': [{'name': 'jack', 'age': 18}, {'name': 'rose', 'age': 18}]}]}
2.2.2 示例3(类的继承)
@ex_dataclass
class Person:
# default_factory: 需要给一个类(可callable)
name: str = field(default_factory=str)
# default: 给定一个默认值
age: int = field(default=0)
height: float = field(default=float)
weight: float = field(default=float)
# 继承person使其拥有person的属性
@ex_dataclass
class Jack(Person):
is_male: bool = field(default=true)
jack = Jack(name="jack", age=18, height=180.0, weight=150.0)
2.2.3 示例4(自定义类的嵌套)
from ex_dataclass import ex_dataclass, field
@ex_dataclass
class PersonDetails:
address: str = field(default_factory=str)
phone: str = field(default_factory=str)
@ex_dataclass
class Person:
# default_factory: 需要给一个类(可callable)
name: str = field(default_factory=str)
# default: 给定一个默认值
age: int = field(default=0)
height: int = field(default=int)
weight: int = field(default=int)
details: PersonDetails = field(default_factory=PersonDetails)
data = {
"name": "jack",
"age": 18,
"height": 180,
"weight": 130,
"details": {
"address": "xxxx",
"phone": "1234567890",
}
}
p = Person(**data)
print(p)
# Person(name='jack', age=18, height=180, weight=130, details=PersonDetails(address='xxxx', phone='1234567890'))
2.2.4 示例5(列表)
- 更多的样例:example
@ex_dataclass
class User:
name: str = field(default_factory=str)
age: int = field(default_factory=int)
@ex_dataclass
class ExampleList_1:
# 默认值设置一个空的list
a_list: typing.List[str] = field(default_factory=list)
users: typing.List[User] = field(default_factory=list)
# 第一种写法
exp_list_1 = ExampleList_1(**{
"a_list": ["a", "b", "c"],
"users" : [
# 两个user字典
{"name": "zhangsan", "age": 18, },
{"name": "lisi", "age": 18, },
]
})
# 可以看到users列表里面都是一个个的user对象
print(exp_list_1)
# :return: ExampleList_1(a_list=['a', 'b', 'c'],
# users=[User(name='zhangsan', age=18),
# User(name='lisi', age=18)])
# exp_list_1.users[0].name // 可以顺利的通过补全获取到值
# 第二种写法
tmp_list = [
# 两个user对象,
User(**{"name": "zhangsan", "age": 18, }),
User(**{"name": "lisi", "age": 18, }),
]
exp_list_2 = ExampleList_1(**{
"a_list": ["a", "b", "c"],
"users" : tmp_list
})
# 结果也是一样的
print(exp_list_2)
# :return: ExampleList_1(a_list=['a', 'b', 'c'],
# users=[User(name='zhangsan', age=18),
# User(name='lisi', age=18)])
上面的几个示例在日常的使用中是足以应对大部分数据模型,下面看看一些进阶的样例,更多的样例会放在源码的 example 中。
2.3 进阶示例
2.3.1 typing.Union 类型支持
- typing.Union 是一个联合类型但仅会符合一个类型;官方解析
- 下面是配合 typing.Union 的特性去生成数据对象,通过匹配自定义类型的字段名与字段属性得分最多的类型将会选为需要转化的类型(
类型优先级从左往右
)
# 多态的类的选择
@ex_dataclass
class Human:
name: str = field(default_factory=str)
age: int = field(default_factory=int)
@ex_dataclass
class Male:
name: str = field(default_factory=str)
age: int = field(default_factory=int)
is_male: bool = field(default_factory=bool)
@ex_dataclass
class Female:
name: str = field(default_factory=str)
age: int = field(default_factory=int)
is_female: bool = field(default_factory=bool)
# 开始
@ex_dataclass(ex_debug=False)
class Table:
# 这里是想生成一个male对象,但数据有可能是以下三种情况
want_male: typing.Union[Human, Male, Female] = field(default_factory=dict)
# want female
want_female: typing.Union[Human, Male, Female] = field(default_factory=dict)
# want human
want_people: typing.Union[Human, Male, Female] = field(default_factory=dict)
tb1 = Table(**{
"want_people": {"name": "human",
"age" : 18,
},
"want_male" : {"name" : "male",
"age" : 19,
"is_male": True, },
"want_female": {"name" : "female",
"age" : 20,
"is_female": True, },
})
# 可以看到ex_dataclass 为这三种数据准确的生成了各自对象
print(tb1)
# Table(want_male=Male(name='male', age=19, is_male=True),
# want_female=Female(name='female', age=20, is_female=True),
# want_people=Human(name='human', age=18))
2.3.2 typing.Type 类型支持
- typing.Type 不太好解析看看官方例子就明白, 大概就是写一个父类其子类都会被匹配;官方解析
# typing.Type 其实通过子类寻找之间的关系
@ex_dataclass
class Human:
name: str = field(default_factory=str)
age: int = field(default_factory=int)
# 使用了继承
@ex_dataclass
class Male(Human):
is_male: bool = field(default_factory=bool)
@ex_dataclass
class Female(Human):
is_female: bool = field(default_factory=bool)
# 开始
@ex_dataclass(ex_debug=False)
class Table2:
# 这的写法只需要标记出它的父类,子类也会被寻找到
want_male: typing.Type[Human] = field(default_factory=dict)
# want female
want_female: typing.Type[Human] = field(default_factory=dict)
# want Human
want_people: typing.Type[Human] = field(default_factory=dict)
tb2 = Table2(**{
"want_people": {"name": "people",
"age" : 18,
},
"want_male" : {"name" : "male",
"age" : 19,
"is_male": True, },
"want_female": {"name" : "female",
"age" : 20,
"is_female": True, },
})
# 可以看到ex_dataclass 为这三种数据准确的生成了各自对象
print(tb2)
# Table2(want_male=Male(name='male', age=19, is_male=True),
# want_female=Female(name='female', age=20, is_female=True),
# want_people=Human(name='people', age=18))
2.3.3 关于typing.Type 和 typing.Union
- 更复杂的用法:magic
2.4 一些补充用法
2.4.1 一些特性 (后续加入新特性也会追加在这
)
- 为 field 加入 required 参数,如果不提供则会抛出错误异常 error.FieldRequiredError。
import typing
from ex_dataclass import ex_dataclass, field, error
@ex_dataclass
class User:
name: str = field(default_factory=str, required=True)
age: int = field(default_factory=int, required=True)
try:
# 没有给出 age 字段
u1 = User(name="u1")
except error.FieldRequiredError as e:
print(e)
# :return: <class 'User'>.age must be required, which is missing.
2.4.2 EXpack
- EXpack 主要包含一些便捷及进阶的用法
2.4.2.1 便捷转化
from ex_dataclass import ex_dataclass, field, error, EXpack
@ex_dataclass
class User(EXpack):
name: str = field(default_factory=str, required=True)
age: int = field(default_factory=int, required=True)
# 从字符串转化成对象
data_str = '''{"name": "jack", "age": 18}'''
jack = User.json_loads(data_str)
print(jack)
# :return: User(name='jack', age=18)
# 对象转化出json数据
print(jack.json_dumps())
# :return: {"name": "jack", "age": 18}
# 对象转化成dict数据
print(jack.asdict())
# :return: {'name': 'jack', 'age': 18}
2.4.2.2 高级方法(展望未来)
-
v1.1.2 更新
支持自定义的数据类型且该数据类型非 ex-dataclass (即纯净的类型,如: datetime.datetime),loads_<field_name> 和 asdict_<filed_name> 这两个函数是开放数据转换过程中hook处理方式;
@ex_dataclass
class User(EXpack):
name: str = field(default_factory=str, required=True)
age: int = field(default_factory=int, required=True)
# 脱离ex-dataclass, 自定义数据类型, 如datetime.datetime;
# 可以通过 loads_<field_name> 和 asdict_<filed_name> 两个钩子函数做转换
created: datetime.datetime = field(default=datetime.datetime.now())
# 当从数据对象转存字典时,我要把created字段的时间对象转换成str,方便后面对数据的处理
def asdict_created(self, value: datetime.datetime) -> str:
return value.strftime("%Y-%m-%d")
# 从字典转化成数据对象时,如我接收是str,转存数据对象的时候要变成datetime.datetime
def loads_created(self, value: str) -> datetime.datetime:
return datetime.datetime.strptime(value, "%Y-%m-%d")
u1 = User(**{"name": "jack", "age": 18, "created": "2021-07-14"})
3. 最后
希望 ex-dataclass 能真正帮到各位同学在开发时减轻维护数据结构的压力;之前在跟一些运维同事沟通的时候,因为开发的水平不一,有时候会挺难理解这样的写法。但这并不是问题,你就当定义一个数据结构和字典相互转换就好。如有疑问可以在下面评论;
联系方式:972367265@qq.com
为仓库给个 🌟 再走吧,https://github.com/Shadow-linux/ex_dataclass