dataclasses库的使用!☃️
dataclasses库的使用
1.简介
Dataclasses
是一些适合于存储数据对象(data object)的Python类。
他们存储并表示特定的数据类型。例如:一个数字。
并且他们能够被用于和同类型的其他对象进行比较。
2.用法
Python3.7 提供了一个装饰器dataclass
,用以把一个类转化为dataclass
。
你需要做的就是把类包裹进装饰器里:
1 |
|
3.初始化
正常的初始化过程:
1 |
|
用dataclass
是这样:
1 |
|
以下是dataclass
装饰器带来的变化:
- 无需定义
__init__
,然后将值赋给self
,dataclass
负责处理它 - 我们以更加易读的方式预先定义了成员属性,以及类型提示。我们现在立即能知道
val
是int
类型。这无疑比一般定义类成员的方式更具可读性。
它也可以定义默认值:
1 |
|
4.表示
对象表示指的是对象的一个有意义的字符串表示,它在调试时非常有用。
默认的 Python 对象表示不是很直观:
1 |
|
这让我们无法知悉对象的作用,并且会导致糟糕的调试体验。
一个有意义的表示可以通过在类中定义一个__repr__
方法来实现。
1 |
|
现在我们得到这个对象有意义的表示:
1 |
|
dataclass
会自动添加一个__repr__
函数,这样我们就不必手动实现它了。
1 |
|
5.数据比较
通常,数据对象之间需要相互比较。
两个对象a
和b
之间的比较通常包括以下操作:
- a < b
- a > b
- a == b
- a >= b
- a <= b
在 Python 中,能够在可以执行上述操作的类中定义方法。为了简单起见,只展示==
和<
的实现。
通常这样写:
1 |
|
使用dataclass
:
1 |
|
我们不需要定义__eq__
和__lt__
方法,因为当order = True
被调用时,dataclass 装饰器
会自动将它们添加到我们的类定义中。
当你使用dataclass
时,它会在类定义中添加函数__eq__
和__lt__
。
生成__eq__
函数的 dataclass 类会比较两个属性构成的元组,一个由自己属性构成的,另一个由同类的其他实例的属性构成。在我们的例子中,自动生成的__eq__
函数相当于:
1 |
|
让我们来看一个更详细的例子:
我们会编写一个dataclass
类Person
来保存name
和age
。
1 |
|
自动生成的__eq__
方法等同于:
1 |
|
请注意属性的顺序。它们总是按照你在dataclass
类中定义的顺序生成。
同样,等效的__le__
函数类似于:
1 |
|
6.dataclass 作为一个可调用的装饰器
定义所有的魔法方法(下一篇详细介绍)并不总是值得的。你的用例可能只包括存储值和检查相等性。因此,你只需定义__init__
和__eq__
方法。如果我们可以告诉装饰器不生成其他方法,那么它会减少一些开销,并且我们将在数据对象上有正确的操作。
幸运的是,这可以通过将dataclass
装饰器作为可调用对象来实现。
装饰器可以用作具有如下参数的可调用对象:
1 |
|
init
:默认将生成__init__
方法。如果传入False
,那么该类将不会有__init__
方法。repr
:__repr__
方法默认生成。如果传入False
,那么该类将不会有__repr__
方法。eq
:默认将生成__eq__
方法。如果传入False
,那么__eq__
方法将不会被dataclass
添加,但默认为object.__eq__
。order
:默认将生成__gt__
、__ge__
、__lt__
、__le__
方法。如果传入False
,则省略它们。
我们在接下来会讨论frozen
。由于unsafe_hash
参数复杂的用例,它值得单独发布一篇文章。
现在回到我们的用例,以下是我们需要的:
- init
- eq
默认会生成这些函数,因此我们需要的是不生成其他函数。那么我们该怎么做呢?很简单,只需将相关参数作为false
传入给生成器即可。
1 |
|
7.Frozen(不可变) 实例
Frozen 实例是在初始化对象后无法修改其属性的对象。
无法创建真正不可变的 Python 对象
以下是我们期望不可变对象能够做到的:
1 |
|
有了dataclass
,就可以通过使用dataclass
装饰器作为可调用对象配合参数frozen=True
来定义一个frozen
对象。
当实例化一个frozen
对象时,任何企图修改对象属性的行为都会引发FrozenInstanceError
。
1 |
|
因此,一个frozen 实例
是一种很好方式来存储:
- 常数
- 设置
这些通常不会在应用程序的生命周期内发生变化,任何企图修改它们的行为都应该被禁止。
8.后期初始化处理
有了dataclass
,需要定义一个__init__
方法来将变量赋给self
这种初始化操作已经得到了处理。但是我们失去了在变量被赋值之后立即需要的函数调用或处理的灵活性。
让我们来讨论一个用例,在这个用例中,我们定义一个Float
类来包含浮点数,然后在初始化之后立即计算整数和小数部分。
通常是这样:
1 |
|
幸运的是,使用post_init方法已经能够处理后期初始化操作。
生成的__init__
方法在返回之前调用__post_init__
返回。因此,可以在函数中进行任何处理。
1 |
|
9.继承
Dataclasses
支持继承,就像普通的Python
类一样。
因此,父类中定义的属性将在子类中可用。
1 |
|
请注意,Student
的参数是在类中定义的字段的顺序。
继承过程中__post_init__
的行为是怎样的?
由于__post_init__
只是另一个函数,因此必须以传统方式调用它:
1 |
|
在上面的例子中,只有B
的__post_init__
被调用,那么我们如何调用A
的__post_init__
呢?
因为它是父类的函数,所以可以用super
来调用它。
1 |
|
10.field
我们已经知道Dataclasses
会生成他们自身的__init__
方法。它同时把初始化的值赋给这些字段。
- 变量名
- 数据类型
这些内容仅给我们有限的dataclass
字段使用范围。让我们讨论一下这些局限性,以及它们如何通过dataclass.field
被解决。
1.复合初始化
1 |
|
数据类Student
产生了一个名为marks
的列表。我们不传递marks
的值,而是使用__post_init__
方法初始化。这是我们定义的单一属性。此外,我们必须在__post_init__
里调用get_random_marks
函数。这些工作是额外的。
辛运的是,Python
为我们提供了一个解决方案。我们可以使用dataclasses.field
来定制化dataclass
字段的行为以及它们在dataclass
的影响。
仍然是上述的使用情形,让我们从__post_init__
里去除get_random_marks
的调用。以下是使用dataclasses.field
的情形:
1 |
|
dataclasses.field
接受了一个名为default_factory
的参数,它的作用是:如果在创建对象时没有赋值,则使用该方法初始化该字段。
default_factory
必须是一个可以调用的无参数方法(通常为一个函数)。
2.使用全部字段进行数据比较
我们了解到,dataclass
能够自动生成<
,=,>
,<=
和>=
这些比较方法。但是这些比较方法的一个缺陷是,它们使用类中的所有字段进行比较,而这种情况往往不常见。更经常地,这种比较方法会给我们使用dataclasses
造成麻烦。
考虑以下的使用情形:你有一个数据类用于存放用户的信息。现在,它可能存在以下字段:
- 姓名
- 年龄
- 身高
- 体重
你仅想比较用户对象的年龄、身高和体重。你不想比较姓名。这是后端开发者经常会遇到的使用情景。
1 |
|
自动生成的比较方法会比较以下的数组:
1 |
|
这将会破坏我们的意图。我们不想让姓名(name)
用于比较。那么,如何使用dataclasses.field
来实现我们的想法呢?
1 |
|
默认情况下,所用的字段都用于比较,因此我们仅仅需要指定哪些字段用于比较,而实现方法是直接把不需要的字段定义为filed(compare=False)
。
3.使用全部字段进行数据表示
自动生成的__repr__
方法使用所有的字段用于表示。当然,这也不是大多数情形下的理想选择,尤其是当你的数据类有大量的字段时。单个对象的表示会变得异常臃肿,对调试来说也不利。
我们也能够个性化这种行为。考虑一个类似的使用场景,也许最合适的用于表示的属性是姓名(name)。那么对__repr__
,我们仅使用它:
1 |
|
4.从初始化中省略字段
目前为止我们看到的所有例子,都有一个共同特点——即我们需要为所有被声明的字段传递值,除了有默认值之外。在那种情形下(指有默认值的情况下),我们可以选择传递值,也可以不传递。
但是,还有一种情形:我们可能不想在初始化时设定某个字段的值。这也是一种常见的使用场景。也许你在追踪一个对象的状态,并且希望它在初始化时一直被设为False
。更一般地,这个值在初始化时不能够被传递。
那么,我们如何实现上述想法呢?以下是具体内容:
1 |
|