Python 数据类

Python 中的数据类可通过三种数据类构建器来构建,分别为 collections.namedtupletyping.NamedTuple 以及 dataclasses.dataclass(前两者还有个名字叫具名元组)。

数据类的一大好处就是已经定义好了一些魔法方法,比如 __str____eq__ 什么的,正常的类你 print 出来的东西是其内存地址(在 CPython 中),而数据类则是构造函数形式展示的字符串;而且正常类你无法对其进行比较(只能比较 id,和 is 差不多了),而数据类则是值相同的实例就能被 == 判断为 True,这就很方便了。刚好可以作为数据的载体(特别是一些数据类是可哈希的(毕竟是不可变的))。

collections.namedtuple

先说 collection 的,它是 tuple 的子类,无法使用 class 字面量来定义,只能通过 DataClass = namedtuple('DataClass', 'data content') 来创建,然后你就可以通过 data_1 = DataClass(data, text) 来创建一个数据类实例(type 为 tuple,因此它不可变)。不过正因为它无法使用 class 字面量定义,所以使用者应该不多(因为这样不好加方法,当然它也是能通过注入方法来加方法的)。不过有一说一,构造函数其实比 class 字面量更快,毕竟 class 字面量是要进行多行检测的。

typing.NamedTuple

接下来是 typing 的,这个数据类一个好处就是方便加类型提示(但是每个属性都需要类型提示)。你可以像 collection 的那样直接用构造函数来创建 DataClass = NamedTuple('DataClass', [('data', dict), ('content', str)]),这和上一个没什么区别。你还可以通过另一种方式构建,也就是 class 字面量,这正是它比上一个的优势所在:

1
2
3
class Dataclass(NamedTuple):
data: dict
content: str

这样的形式你就可以相当方便地添加一些方法了,而且可读性也更高(其中还有默认值相关未介绍,其实默认值没什么好说的,content: str = '' 就能创建默认值了)。当然,你看到 (NamedTuple) 后可能会认为这是继承,这其实是一个误区,DataClass 并不是 NamedTuple 的子类,它还是 tuple 的子类,而 NamedTuple 实际上是一个元类(元类这玩意比较复杂,具体是什么我还没学捏),所以本质上这种行为和继承无任何关系。

dataclasses.dataclass

最后是 dataclasses 的了,我感觉这个应该是最合适使用的了,它就不是 tuple 的子类了,它父类是 object,和正常的类一致,这也表示它其实是可变的(当然你也可以让它不可变,但是你还是可以通过一些方法来改变其中的内容)。它无法通过构造函数来构建,而是使用了类装饰器:

1
2
3
4
@dataclass
class DataClass:
data: dict
content: str

如果你需要不可变的就要给装饰器加上参数 @dataclass(frozen=True),这样就无法给它构造的类加属性了(但是这种不可变还是比较局限的)。你可以像 content: str = '' 这样来给它加上默认值,但需要注意的是,content: str = '' 创建的为实例属性,而不是类属性;但是如果你是像 content = '' 这样创建的话,它就又成了类属性,这是需要警惕的。如果你想要类型注释和默认值但又要类属性,你就需要 typing.ClassVar,通过 content: ClassVar[str] = '' 创建的就是类属性啦。同时,数据类如果要使用可变的容器(如 dictlist)的话,不能直接将它们作为默认值,而是需要使用 dataclasses.field(default_factory=list) 来创建默认值,比如 data: dict = field(default_factory=dict),这样能防止实例共用属性,直接用 [] 的话会直接报错。

除此之外,有关数据类其实有个说法,就是,数据类属于一种代码“异味”,正如一句话所说“数据类就像一个小孩子,如果要在一个复杂的系统使用,数据类就得承担一些责任”,这句话所表示的也就是不要把数据类当作一个单纯的数据的载体,有关里面数据的调用都应当由数据类自身的方法实现,而不应该在外部创建一个函数来处理。数据类固然好用,但是也不能滥用。