dataclass数据类

python 数据类:dataclass

作者: elfin   参考资源: python3.7的新特性dataclass   Python 3.7 将引入 dataclass 装饰器


Top  ---  Bottom


1、dataclass简介

​ dataclass是python3.7开始带有的新属性(类装饰器),dataclass是指”一个带有默认值的可变namedtuple“,本质还是一个类,它的属性非特殊情况可以直接访问,类中有与属性相关的类方法。简单地说就是一个含有数据及其操作方法的类。

dataclass与普通类的区别

  • 与普通类相比,dataclass通常不包含私有属性,这些属性可以直接访问(也可以私有);
  • repr() 函数将对象转化为供解释器读取的形式;dataclass的repr方法通常有其固定格式,会打印类名、属性名、属性值;
  • dataclass有__eq____hash__这些魔法方法;
  • dataclass有着模式单一固定的构造方式,根据需要有时需要重载运算符,而普通class通常无需这些工作。

注:namedtuple是tuple的子类,它的元素是有命名的!


Top  ---  Bottom

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装饰器的一些知识点。


Top  ---  Bottom

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__方法;
      1. eq、frozen都为True时,__hash__将会生成;
      2. eq为True,frozen为False,__hash__将被设置为None;
      3. eq为False,frozen为True,__hash__将使用object(超类)的同名属性(通常就是对象id的hash)
    • 当unsafe_hash为True时,将会根据类的属性生成__hash__。如其名,这是不安全的,因为属性是可变的,这会导致hash的不一致。当然您能保证对象属性不会变,你也可以设置为True。
  • frozen控制是否冻结对field赋值。设置为True时,对象将是不可变的,因为不可变,所以如果设置有__setattr____delattr__将会导致TypeError错误。

前两个参数我们在上一章实际已经看了效果,下面我们查看参数eqorder

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


Top  ---  Bottom

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“,但是我们访问不到。


Top  ---  Bottom

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

Top  ---  Bottom

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定义。


Top  ---  Bottom

7、小结

​ 合理使用dataclass将会大大减轻开发中的负担,将我们从大量的重复劳动中解放出来,这既是dataclass的魅力,不过魅力的背后也总是有陷阱相伴,最后我想提几点注意事项:

  • dataclass通常情况下是unhashable的,因为默认生成的__hash__None,所以不能用来做字典的key,如果有这种需求,那么应该指定你的数据类为frozen dataclass
  • 小心当你定义了和dataclass生成的同名方法时会引发的问题
  • 当使用可变类型(如list)时,应该考虑使用fielddefault_factory
  • 数据类的属性都是公开的,如果你有属性只需要初始化时使用而不需要在其他时候被访问,请使用dataclasses.InitVar

​ 只要避开这些陷阱,dataclass一定能成为提高生产力的利器。


后记:最近看别人的代码,经常使用dataclass,但是我对这个特性“并不熟悉”,记忆中好像自己学过这个知识点,但是又没有深刻的记忆。百度了这个知识点,结果百度出的第一篇博客居然是我写的,简直离谱。想想自己最近写的屎山代码,内心一群xxxxx飞奔。鉴于最近想排除相同的字典,而字典默认是不能hash的,虽然有其他成熟的“字典库”作为替换,但是这里dataclass也可以解决我的问题,因为我的key是固定的,所以适合自定义对象。至此,我想到使用pydantic和dataclass进行实现,所以再次查询了dataclass资料,这个视频讲解还是非常不错的,建议初学者参考。


Top  ---  Bottom

完!

posted @ 2021-05-19 19:10  巴蜀秀才  阅读(2657)  评论(0编辑  收藏  举报