dataclass数据类
python 数据类:dataclass
作者: elfin 参考资源: python3.7的新特性dataclass Python 3.7 将引入 dataclass 装饰器
1、dataclass简介
dataclass是python3.7开始带有的新属性(类装饰器),dataclass是指”一个带有默认值的可变namedtuple“,本质还是一个类,它的属性非特殊情况可以直接访问,类中有与属性相关的类方法。简单地说就是一个含有数据及其操作方法的类。
dataclass与普通类的区别
- 与普通类相比,dataclass通常不包含私有属性,这些属性可以直接访问(也可以私有);
- repr() 函数将对象转化为供解释器读取的形式;dataclass的repr方法通常有其固定格式,会打印类名、属性名、属性值;
- dataclass有
__eq__
、__hash__
这些魔法方法; - dataclass有着模式单一固定的构造方式,根据需要有时需要重载运算符,而普通class通常无需这些工作。
注:namedtuple是tuple的子类,它的元素是有命名的!
2、引入dataclass装饰器
常见的类生成方式
class elfin:
def __init__(self, name, age):
self.name = name
self.age = age
使用dataclass装饰器
@dataclass
class elfin:
name: str
age: int
我们使用@dataclass
就可以实现与普通类的效果,这样代码更简洁!
__post_init__
方法
如果某个属性需要在init后处理,就可以放置到__post_init__
中!
@dataclass
class elfin:
name: str
age: int
def __post_init__(self):
if type(self.name) is str:
self.identity = identity_dict[self.name]
测试上面的案例:
>>> from dataclasses import dataclass
>>> identity_dict = {
... "firstelfin": "boss",
... "secondelfin": "master",
... "thirdelfin": "captain"
... }
>>> @dataclass
... class Elfin:
... name: str
... age: int
...
... def __post_init__(self):
... if type(self.name) is str:
... self.identity = identity_dict[self.name]
>>> print(Elfin)
... Out[1]: <class '__main__.Elfin'>
>>> elfin_ins = Elfin("firstelfin", 23)
>>> elfin_ins
... Out[2]: Elfin(name='firstelfin', age=23)
>>> elfin_ins.identity
... Out[3]: 'boss'
上面的案例向我们展示了即使init部分没有生成identity属性,实例也可以获取到!
下面我们就分别展示dataclass装饰器的一些知识点。
3、dataclass装饰器选项
使用dataclass类装饰器的选项,我们可以定制我们想要的数据类,默认选项为:
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Elfin:
pass
装饰器的参数选项说明:
- init控制是否生成
__init__
方法; - repr控制是否生成
__repr__
方法; - eq控制是否生成
__eq__
方法,它用于判断实例是否相等; - order控制是否创建四种大小关系方法:
__lt__
、__le__
、__gt__
、__ge__
;order为True,则eq不能为False,也不能自定义order方法。 - unsafe_hash控制hash的生成方式。
- 当unsafe_hash为False时,将根据eq、frozen参数来生成
__hash__
方法;- eq、frozen都为True时,
__hash__
将会生成; - eq为True,frozen为False,
__hash__
将被设置为None; - eq为False,frozen为True,
__hash__
将使用object(超类)的同名属性(通常就是对象id的hash)
- eq、frozen都为True时,
- 当unsafe_hash为True时,将会根据类的属性生成
__hash__
。如其名,这是不安全的,因为属性是可变的,这会导致hash的不一致。当然您能保证对象属性不会变,你也可以设置为True。
- 当unsafe_hash为False时,将根据eq、frozen参数来生成
- frozen控制是否冻结对field赋值。设置为True时,对象将是不可变的,因为不可变,所以如果设置有
__setattr__
、__delattr__
将会导致TypeError
错误。
前两个参数我们在上一章实际已经看了效果,下面我们查看参数eq
、order
:
>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
... name: str
... age: int
...
... def __post_init__(self):
... if type(self.name) is str:
... self.identity = identity_dict[self.name]
>>> elfin_ins1 = Elfin("thirdelfin", 18)
>>> elfin_ins2 = Elfin("secondelfin", 20)
>>> elfin_ins1 == elfin_ins2
... Out[4]: False
>>> elfin_ins1 >= elfin_ins2
... Out[5]: True
>>>
可以发现我们可以在实例之间进行大小的比较了!同时我们知道普通类是不同进行大小比较的:
>>> class A:
... def __init__(self, age):
... self.age = age
>>> a1 = A(20)
>>> a2 = A(30)
>>> a1 > a2
... TypeError Traceback (most recent call last)
... <ipython-input-24-854e76ddfa09> in <module>
... ----> 1 a1 > a2
...
... TypeError: '>' not supported between instances of 'A' and 'A'
上面我们提到了field,实际上,所有的数据类属性,都是被field所控制,它代表一个数据的实体和它的元信息,下面我们了解一下dataclasses.field
。
4、数据类的基石--dataclasses.field
field的定义如下:
def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
hash=None, compare=True, metadata=None):
if default is not MISSING and default_factory is not MISSING:
raise ValueError('cannot specify both default and default_factory')
return Field(default, default_factory, init, repr, hash, compare,
metadata)
一般情况下,我们无需直接使用,装饰器会根据我们给出的类型注解自动生成field,但有时候也需要定制这个过程,所以dataclasses.field
就特别重要了!
参数说明:
-
default:如果调用时没有指定,则默认为None,它控制的是field的默认值;
-
default_factory:控制如何产生值,它接收一个无参数或者全是默认参数的
callable
对象,然后调用该对象field的初始值,再将default复制给callable
对象。 -
init:控制是否在init中生成此参数。在前面章节的案例中,我们要生成
self.identity
属性,但是不想在init中传入,就可以使用field了。>>> @dataclass(init=True, repr=True, eq=True, order=True) ... class Elfin: ... name: str ... age: int ... identity: str = field(init=False) ... ... def __post_init__(self): ... if type(self.name) is str: ... self.identity = identity_dict[self.name] >>> elfin_ins3 = Elfin("firstelfin", 20) >>> elfin_ins3 ... Out[6]: Elfin(name='firstelfin', age=20, identity='boss')
-
repr:表示该field是否被包含进repr的输出,默认要输出,如上面的案例。
-
compare:是否参与比较和计算hash值。
-
hash:是否参与比较和计算hash值。
-
metadata不被dataclass自身使用,通常让第三方组件从中获取某些元信息时才使用,所以我们不需要使用这一参数。
只能初始化调用的属性
如果指定一个field的类型注解为dataclasses.InitVar
,那么这个field将只会在初始化过程中(__init__
和__post_init__
)可以被使用,当初始化完成后访问该field会返回一个dataclasses.Field
对象而不是field原本的值,也就是该field不再是一个可访问的数据对象。
>>> from dataclasses import InitVar
>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
... name: str
... age: int
... identity: InitVar[str] = None
...
... def __post_init__(self, identity):
... if type(self.name) is str:
... self.identity = identity_dict[self.name]
>>> elfin_ins3 = Elfin("firstelfin", 20)
>>> elfin_ins3
... Out[7]: Elfin(name='firstelfin', age=20)
>>> elfin_ins3.identity
>>>
注意这里elfin_ins3.identity说明都没有返回,实际上应该是”boss“,但是我们访问不到。
5、dataclass的常用函数
5.1 转换数据为字典 dataclasses.asdict
>>> from dataclasses import asdict
>>> asdict(elfin_ins3)
... Out[8]: {'name': 'firstelfin', 'age': 20}
5.2 转换数据为元组 dataclasses.astuple
>>> from dataclasses import astuple
>>> astuple(elfin_ins3)
... Out[9]: ('firstelfin', 20)
5.3 判断是否是dataclass类
>>> from dataclasses import is_dataclass
>>> is_dataclass(Elfin)
... Out[10]: True
>>> is_dataclass(elfin_ins3)
... Out[11]: True
6、dataclass继承
python3.7引入dataclass的一大原因就在于相比namedtuple,dataclass可以享受继承带来的便利。
dataclass
装饰器会检查当前class的所有基类,如果发现一个dataclass,就会把它的属性按顺序添加进当前的class,随后再处理当前class的field。所有生成的方法也将按照这一过程处理,因此如果子类中的field与基类同名,那么子类将会无条件覆盖基类。子类将会根据所有的field重新生成一个构造函数,并在其中初始化基类。
案例:
>>> @dataclass(init=True, repr=True, eq=True, order=True)
... class Elfin:
... name: str = "firstelfin"
... age: int = 20
... identity: InitVar[str] = None
...
... def __post_init__(self, identity):
... if type(self.name) is str:
... self.identity = identity_dict[self.name]
>>> @dataclass
... class Wude(Elfin):
... age: int = 68
>>> Wude()
... Out[11]: Wude(name='firstelfin', age=68)
>>>
上述可见,Wude类继承了Elfin类的name属性,而实例中的age覆盖了Elfin中的age定义。
7、小结
合理使用dataclass将会大大减轻开发中的负担,将我们从大量的重复劳动中解放出来,这既是dataclass的魅力,不过魅力的背后也总是有陷阱相伴,最后我想提几点注意事项:
- dataclass通常情况下是unhashable的,因为默认生成的
__hash__
是None
,所以不能用来做字典的key,如果有这种需求,那么应该指定你的数据类为frozen dataclass - 小心当你定义了和
dataclass
生成的同名方法时会引发的问题 - 当使用可变类型(如list)时,应该考虑使用
field
的default_factory
- 数据类的属性都是公开的,如果你有属性只需要初始化时使用而不需要在其他时候被访问,请使用
dataclasses.InitVar
只要避开这些陷阱,dataclass一定能成为提高生产力的利器。
后记:最近看别人的代码,经常使用dataclass,但是我对这个特性“并不熟悉”,记忆中好像自己学过这个知识点,但是又没有深刻的记忆。百度了这个知识点,结果百度出的第一篇博客居然是我写的,简直离谱。想想自己最近写的屎山代码,内心一群xxxxx飞奔。鉴于最近想排除相同的字典,而字典默认是不能hash的,虽然有其他成熟的“字典库”作为替换,但是这里dataclass也可以解决我的问题,因为我的key是固定的,所以适合自定义对象。至此,我想到使用pydantic和dataclass进行实现,所以再次查询了dataclass资料,这个视频讲解还是非常不错的,建议初学者参考。
完!