Python 数据类 dataclasses 实践

简介

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

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

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

一个基础例子:

@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 用来将类转化为数据类。该装饰器的原型定义如下:

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

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

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

@dataclasses.dataclass
class Person:
    name: str
    age: int = 20

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

我们可以这样去使用:

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 我们可以对参数做更多的定制化,如:

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

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

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

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

实例说明

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

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

{
    "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"
    }
}

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

@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)

将数据传入到数据类中:

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

对数据进行操作:

# 获取数据
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 的数据类实现:

@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 属性.

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 数据类进行初始化。

@dataclass
class Page:
    ...  # 上方的属性

    def __post_init__(self):
        dicts_to_dataclasses(self)

上述完整代码参见 demo2

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

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

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

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

参考资料

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

推荐阅读更多精彩内容