[Python] 如何降低维护代码成本 (ex-dataclass)

源码地址:https://github.com/Shadow-linux/ex_dataclass

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 中数据模型非常难以维护的问题,这个问题在稍微大点的项目中都让人非常的头疼。其实在早前官方推出过 dataclasstyping 两个内置库来解决这个难题,但效果不明显,甚至很多人都完全不知道。最重要的是 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.Listex_dataclass 类型正反解析;
  • 支持 typing.List 嵌套正反解析,如:{a: [[{a:1, b:2}, {a:3, b:4}]]}
  • 支持 typing.Uniontyping.Type 特殊容器类型注解的多态行为,精确匹配字段存在最多 ex_dataclass 类;
  • 支持反向解析下存在冗余字段,并且抛弃该字段
  • 支持typing.Union 和 typing.Type 特殊容器类型相互嵌套场景 ;
  • 支持纯净的class类型的正反转换解析;
  • 支持 ex_dataccass 字段检测校验,通过类型注解获取类后进行值的校验;

2.2 快速开始

  • 安装
pip install ex-dataclass

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(列表)


@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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容

  • 1、数据库介绍篇 1.1什么是数据库 数据库:保存数据的仓库。它体现我们电脑中,就是一个文件系统。然后把数据都保存...
    投石机阅读 711评论 0 0
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,812评论 0 38
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,536评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,190评论 4 8
  • 步骤:发微博01-导航栏内容 -> 发微博02-自定义TextView -> 发微博03-完善TextView和...
    dibadalu阅读 3,138评论 1 3