avatar

Python 数据类 dataclasses 实践

Python3.7 版本开始,引入了一个新的模块 dataclasses,该模块主要提供了一种数据类的数据类的实现方式。基于 PEP-557实现。 所谓数据类,类似 Java 语言中的 Bean。通过一个容器类(class),继而使用对象的属性访问数据。

如果你使用过标准库中的 collections.namedtuple, 或者 typing.NamedTupledataclasses是与这两者类似的。

通过 dataclasses 我们可以更加方便的去定义一个数据类。并且可以通过原生的方式进行类型检查。

一个基础例子:

1
2
3
4
5
6
7
8
9
@dataclass
class InventoryItem:
'''Class for keeping track of an item in inventory.'''
name: str
unit_price: float
quantity_on_hand: int = 0

def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand

基础用法

dataclasses 提供一个模块级的装饰器 dataclass 用来将类转化为数据类。该装饰器的原型定义如下:

1
@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

提供的默认参数用来控制是否生成相应的魔术方法。如 reprTrue 时,将会自动生成 __repr__ 方法。

我们定义一个简单的数据类,用以实现一个使用对象的属性存储实体 Person 数据:

1
2
3
4
@dataclasses.dataclass
class Person:
name: str
age: int = 20

该类中定义了两个属性 nameage。分别表示名称和年龄,并且说明 name 属性是一个字符串,age 属性是一个数字(注意: 因为 Python 编译器不会对此处的类型进行强制检查),并为 age 属性设置了默认值 20

我们可以这样去使用:

1
2
3
4
5
6
7
8
In [1]: person = Person('ikaros', 24)

In [2]: person.name
Out[2]: 'ikaros'

# 因为默认情况下 `repr` 是自动生成的,所以我们得到 `person` 的字符串表示。
In [3]: person
Out[3]: Person(name='ikaros', age=24)

通过使用 field 我们可以对参数做更多的定制化,如:

1
2
3
4
@dataclasses.dataclass
class Person:
name: str
age: int = dataclasses.field(default=20, repr=False)

此处我们为 age 属性赋予了一个额外的 reprFalse 的参数。该参数说明,在调用 __repr__ 方法时,不展示 age 属性:

1
2
In [4]: person
Out[4]: Person(name='ikaros')

更多的 field 说明,可以查看 参考文档

实例说明

此处我们通过一个实际的例子展示 dataclasses 的用法.

现有一个数据实体内部的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"id": "20531316728",
"about": "The Facebook Page celebrates how our friends inspire us, support us, and help us discover the world when we connect.",
"birthday": "02/04/2004",
"name": "Facebook",
"username": "facebookapp",
"fan_count": 214643503,
"cover": {
"cover_id": "10158913960541729",
"offset_x": 50,
"offset_y": 50,
"source": "https://scontent.xx.fbcdn.net/v/t1.0-9/s720x720/73087560_10158913960546729_8876113648821469184_o.jpg?_nc_cat=1&_nc_ohc=bAJ1yh0abN4AQkSOGhMpytya2quC_uS0j0BF-XEVlRlgwTfzkL_F0fojQ&_nc_ht=scontent.xx&oh=2964a1a64b6b474e64b06bdb568684da&oe=5E454425",
"id": "10158913960541729"
}
}

我们通过定义一个对应的数据类来表示该数据实体:

1
2
3
4
5
6
7
8
9
@dataclass
class Page:
id: str = None
about: str = field(default=None, repr=False)
birthday: str = field(default=None, repr=False)
name: str = None
username: str = None
fan_count: int = field(default=None, repr=False)
cover: dict = field(default=None, repr=False)

将数据传入到数据类中:

1
2
# data 为 上述的数据
In [5]: p = Page(**data)

对数据进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取数据
In [6]: p.name
Out[6]: 'Facebook'

# 字符串展示
In [7]: p
Out[8]: Page(id='20531316728', name='Facebook', username='facebookapp')

In [9]: p.cover
Out[9]:
{'cover_id': '10158913960541729',
'offset_x': 50,
'offset_y': 50,
'source': 'https://scontent.xx.fbcdn.net/v/t1.0-9/s720x720/73087560_10158913960546729_8876113648821469184_o.jpg?_nc_cat=1&_nc_ohc=bAJ1yh0abN4AQkSOGhMpytya2quC_uS0j0BF-XEVlRlgwTfzkL_F0fojQ&_nc_ht=scontent.xx&oh=2964a1a64b6b474e64b06bdb568684da&oe=5E454425',
'id': '10158913960541729'}

上述完整代码参见 demo1

我们在上述的代码发现, 在调用 p.cover 属性时,返回的是一个字典,在正常的使用时,我们是想将 cover 属性也声明为一个数据类。则需要对上述的代码进行修改。

添加一个 Cover 的数据类实现:

1
2
3
4
5
6
7
8
9
10
11
12
@dataclass
class Cover:
id: str = None
cover_id: str = None
offset_x: str = field(default=None, repr=False)
offset_y: str = field(default=None, repr=False)
source: str = field(default=None, repr=False)

@dataclass
class Page:
... # 此处不再复制上方的属性
cover: Cover = field(default=None, repr=False) # 修改 `cover` 属性

但是这时候,如果我们按照刚才的初始化方式,cover 属性不会被识别到。

我们可以通过添加一个额外的初始化的方法用来初始化到 cover 属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
def dicts_to_dataclasses(instance):
"""将所有的数据类属性都转化到数据类中"""
cls = type(instance)
for f in fields(cls):
if not is_dataclass(f.type):
continue

value = getattr(instance, f.name)
if not isinstance(value, dict):
continue

new_value = f.type(**value)
setattr(instance, f.name, new_value)

并且修改上层数据类 Page 的代码,添加一个 __post_init__ 方法, 该方法会被自动生成的 __init__ 方法调用,进而将 Cover 数据类进行初始化。

1
2
3
4
5
6
@dataclass
class Page:
... # 上方的属性

def __post_init__(self):
dicts_to_dataclasses(self)

上述完整代码参见 demo2

此时我们去初始化时,便可以将子数据类 Cover 也初始化了。

1
2
In [10]: p.cover
Out[10]: Cover(id='10158913960541729', cover_id='10158913960541729')

此外,dataclasses 还提供了对数据类到字典的转化。

1
2
3
4
5
6
In [11]: from dataclasses import asdict
In [12]: asdict(p)
Out[12]:
{'id': '20531316728',
....
}

我们可以对上边的代码进行整合一下。将通用的一些函数放到一个 base 基类中。

完整代码参见 demo3

第三方增强库

上边我们只是对含有嵌套字典的复杂数据进行了处理。事实上,生产中的数据的样式会更加复杂。我们根据需求自行对 dicts_to_dataclasses 函数进行升级处理,或者使用第三方库进行处理。

此处我们以第三方库 dataclasses-json 来给出一个示例,详细代码参见 demo-with-dataclasses-json

参考资料

文章作者: Ikaros Kun
文章链接: https://blog.ikaroskun.xyz/2019/11/23/lan-python-dataclassess/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ikaros の小屋