[Python] dataclass 的介绍与优化版本

2,189 阅读5分钟

1. 背景

  • 在编写Python代码的时候,我们会经常遇到数据结构复杂时无法很好的通过代码层面进行记录,只能使用大量的注释去记录这些复杂的数据结构;后面Python3引入了类型标注(类型注解)也无法很好的解决这个问题,继而在Python3.5的时候引入了typing模块增强模块的注解方式。但是面对复杂结构的数据模型,在代码层面还是没有很好的被解决,直至Python3.7dataclass的出现。
  • dataclass 是什么?看看官方是如何定义的PEP 557根据定义一个dataclass是指“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为dataclass。 再通俗点讲,dataclass就是一个含有数据及操作数据方法的容器。

2.dataclass的使用

  • 先简单介绍 dataclass 的用法以及为什么使用 dataclass 可以解决复杂数据结构难以维护的问题和减轻开发压力;

2.1 Example-01

  • 可以当作一个类来使用即可;
import typing
from dataclasses import dataclass, asdict, field

@dataclass
class User:
    name: str = field(default_factory=str)
    age: int = field(default=18)
    
    # 为数据容器提供一个方法
    def say_hello(self):
        print(f"{self.name}: hello")

# 实例化
u = User(name="shadow", age=20)
print(u)
print(u.name)
print(u.age)
print(u.say_hello())
print(asdict(u))

# 输出
>> User(name='shadow', age=20)
>> shadow
>> 20
>> shadow: hello
>> {'name': 'shadow', 'age': 20}
  • 在属性nameage 后面加入类型标注,在实例化时dataclass会对类型进行检测失败的话会抛出异常;
  • default_factory 需要传入的是一个默认工厂类型,可以理解为即可。default 是给定一个默认值;

通过上述例子,我们可以看到数据被对象化,那么IDE等编写代码工具就能为我们提示该数据结构的属性及其方法;

2.2 Example-02

  • 继承与自定义类型标注
import typing
from dataclasses import dataclass, asdict, field

@dataclass
class BasicUser:
    id: int = field(default_factory=int)

@dataclass
class Job:
    position: str = field(default_factory=str)

@dataclass
class User(BasicUser):
    name: str = field(default_factory=str)
    age: int = field(default=18)
    # 使用自己定义的类型来标注
    job: Job = field(default_factory=Job)
    

u = User(name="shadow", age=18, job=Job(position='ops'))
print(u)
print(asdict(u))

# 输出
>>
>> User(id=0, name='shadow', age=18, job=Job(position='ops'))
>> {'id': 0, 'name': 'shadow', 'age': 18, 'job': {'position': 'ops'}}
  • 这里可以看到User在实例化的时候并未传入id, 因此它会使用我们提供的工厂类int进行实例化所以id=0

2.3 Example-03

  • 列表使用
import typing
from dataclasses import dataclass, asdict, field

@dataclass
class User():
    name: str = field(default_factory=str)
    age: int = field(default=18)
    # job: Job = field(default_factory=Job)


@dataclass
class Group:
    # 定义了一个ops_group列表,列表里面的属性标记为User类型
    ops_group: typing.List[User] = field(default_factory=typing.List)

u = User(name="shadow", age=20)
u1 = User(**{"name": "shadow1"})

g = Group(ops_group=[u, u1])
g1 = Group(**{"ops_group": [{'name': 'shadow', 'age': 18}]})

print(g.ops_group)
print(asdict(g))

print(g1)
print(asdict(g1))

# 输出
>> Group(ops_group=[User(name='shadow', age=20), User(name='shadow1', age=18)])
>> {'ops_group': [{'name': 'shadow', 'age': 20}, {'name': 'shadow', 'age': 18}]}
>>
>> Group(ops_group=[{'name': 'shadow', 'age': 18}])
>> {'ops_group': [{'name': 'shadow', 'age': 18}]}
  • 乍一看好像没什么毛病能正常解析列表也能反解析出dict,可以留意一下g1的输出,ops_group列表内的User对象并没有被解析出来,仅仅只给了ops_group一个列表;

上述例子可以看出dataclass虽然表现已经很出色,但还没完全满足我们解决复杂结构的转换;

3.问题与优化

3.1 问题

除了上述的Example-03描述的问题外,dataclass还有存在另外一个显眼的问题;


@dataclass
class User():
    name: str = field(default_factory=str)
    age: int = field(default=18)

# 假如id是冗余参数
u1 = User(**{"name": "shadow1", "age": 21, "id": 1})
print(u1)

# 输出
>> TypeError: __init__() got an unexpected keyword argument 'id'
  • 由于我们在实例化User的时候多给了个id属性,会导致实例化不成功;这种场景其实我们在请求外部系统的时候会经常遇到冗余的响应参数,如果不解决这个问题会导致dataclass易用性降低;

3.2 优化版本

3.2.1 ex_dataclass 使用示例

import typing
from dataclasses import dataclass, is_dataclass, asdict, field

# 改为ex_dataclass
@ex_dataclass
class User():
    name: str = field(default_factory=str)
    age: int = field(default=18)
    
@ex_dataclass
class Group:
    ops_group: typing.List[User] = field(default_factory=typing.List)
    
u1 = User(**{"name": "shadow1", "age": 21, "id": 1})
print(u1)

g1 = Group(**{"ops_group": [{'name': 'shadow', 'age': 18}]})
print(g1)
print(asdict(g1))

# 输出
>>
>> User(name='shadow1', age=21)
>> Group(ops_group=[User(name='shadow', age=18)])
>> {'ops_group': [{'name': 'shadow', 'age': 18}]}
  • u1并没有解析多余的id参数
  • g1ops_group内的User对象也能正常解析;

3.2.2 完整示例

  • 提示:需要把ex_dataclass copy 当前到代码中;
import typing
from dataclasses import dataclass, is_dataclass, asdict, field

# ex_dataclass...

@ex_dataclass
class C:
    c1: typing.List[str] = field(default_factory=list)

@ex_dataclass
class B:
    b1: str = field(default_factory=str)
    c: C = field(default_factory=C)


@ex_dataclass
class A1(B):
    a1_1: str = field(default_factory=str)
    a1_2: int = field(default_factory=int)

@ex_dataclass
class A:
    a1: A1 = field(default_factory=A1)


test_data = {
    "a0": 1,
    "a1": {
        "a1_1": "1",
        "a1_2": 2,
        "b1": "1",
        "c": {
            "c1": ["a", "b", "c"]
        }
    }
}

a = A(**test_data)
print(a)
print(a.a1)
print(a.a1.a1_1)
print(a.a1.c.c1)

# 输出
>> A(a1=A1(b1='1', c=C(c1=['a', 'b', 'c']), a1_1='1', a1_2=2))
>> A1(b1='1', c=C(c1=['a', 'b', 'c']), a1_1='1', a1_2=2)
>> 1
>> ['a', 'b', 'c']
  • 可以看到所有数据都能正常转化成对象数据

4.最后

  • ex_dataclass 协助下后面的代码维护遇到复杂数据结构与多模块传递数据结构时,我们维护与开发的压力都能大幅度降低;
  • 如遇到使用问题可以在下面评论👇;