本文将带你走进python3.7的新特性dataclass,通过本文你将学会dataclass的使用并避免踏入某些陷阱。
乍一看可能会觉得这个概念不就是普通的class么,然而还是有几处不同:
- 相比普通class,dataclass通常不包含私有属性,数据可以直接访问
- dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值
- dataclass拥有
__eq__
和__hash__
魔法方法 - dataclass有着模式单一固定的初始化方式,或是需要重载运算符,而普通class通常无需这些工作
基于上述原因,通常自己实现一个dataclass是繁琐而无聊的,而dataclass单一固定的行为正适合程序为我们自动生成,于是dataclasses
模块诞生了。
配合类型注解语法,我们可以轻松生成一个实现了__init__
,__repr__
,__cmp__
等方法的dataclass:
from dataclasses import dataclass@dataclass
class InventoryItem:
'''Class for keeping track of an item in inventory.'''
name: str
unit_price: float
quantity_on_hand: int = 0def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand</code></pre>
同时使用dataclass也有一些好处,它比namedtuple更灵活。同时因为它是一个常规的类,所以你可以享受继承带来的便利。
我们分x步介绍dataclass的使用,首先是如何定义一个dataclass。
dataclasses
模块提供了一个装饰器帮助我们定义自己的数据类:@dataclass class Lang: """a dataclass that describes a programming language""" name: str = 'python' strong_type: bool = True static_type: bool = False age: int = 28我们定义了一个描述某种程序语言特性的数据类——
Lang
,在接下来的例子中我们都会用到这个类。在数据类被定义后,会根据给出的类型注解生成一个如下的初始函数:
def __init__(self,name: str='python',strong_type: bool=True,static_type: bool=False,age: int=28): self.name = name self.strong_type = strong_type self.static_type = static_type self.age = age可以看到初始化操作都已经自动生成了,让我们试用一下:
>>> Lang() Lang(name='python',strong_type=True,static_type=False,age=28) >>> Lang('js',False,23) Lang(name='js',strong_type=False,age=23) >>> Lang('js',23) == Lang() False >>> Lang('python',True,28) == Lang() True例子中可以看出
__repr__
和__eq__
方法也已经为我们生成了,如果没有其他特殊要求的话这个dataclass已经具备了投入生产环境的能力,是不是很神奇?
dataclass的魔力源泉都在
dataclass
这个装饰器中,如果想要完全掌控dataclass的话那么它是你必须了解的内容。装饰器的原型如下:
dataclasses.dataclass(*,init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False)
dataclass
装饰器将根据类属性生成数据类和数据类需要的方法。我们的关注点集中在它的
kwargs
上:
含义 |
---|
上面我们偶尔提到了field的概念,我们所说的数据类属性,数据属性实际上都是被field的对象,它代表着一个数据的实体和它的元信息,下面我们了解一下dataclasses.field
。
先看下field的原型:
dataclasses.field(*,default=MISSING,default_factory=MISSING,hash=None,compare=True,metadata=None)
通常我们无需直接使用,装饰器会根据我们给出的类型注解自动生成field,但有时候我们也需要定制这一过程,这时dataclasses.field
就显得格外有用了。
default和default_factory参数将会影响默认值的产生,它们的默认值都是None,意思是调用时如果为指定则产生一个为None的值。其中default是field的默认值,而default_factory控制如何产生值,它接收一个无参数或者全是默认参数的callable
对象,然后用调用这个对象获得field的初始值,之后再将default(如果值不是MISSING)复制给callable
返回的这个对象。
举个例子,对于list,当复制它时只是复制了一份引用,所以像dataclass里那样直接复制给实例的做法的危险而错误的,为了保证使用list时的安全性,应该这样做:
@dataclass class C: mylist: List[int] = field(default_factory=list)
当初始化C
的实例时就会调用list()
而不是直接复制一份list的引用:
>>> c1 = C() >>> c1.mylist += [1,2,3] >>> c1.mylist [1,3] >>> c2 = C() >>> c2.mylist []
数据污染得到了避免。
init参数如果设置为False,表示不为这个field生成初始化操作,dataclass提供了hook——__post_init__
供我们利用这一特性:
@dataclass class C: a: int b: int c: int = field(init=False)def __post_init__(self): self.c = self.a + self.b</code></pre>
__post_init__
在__init__
后被调用,我们可以在这里初始化那些需要前置条件的field。repr参数表示该field是否被包含进repr的输出,compare和hash参数表示field是否参与比较和计算hash值。metadata不被dataclass自身使用,通常让第三方组件从中获取某些元信息时才使用,所以我们不需要使用这一参数。
如果指定一个field的类型注解为
dataclasses.InitVar
,那么这个field将只会在初始化过程中(__init__
和__post_init__
)可以被使用,当初始化完成后访问该field会返回一个dataclasses.Field
对象而不是field原本的值,也就是该field不再是一个可访问的数据对象。举个例子,比如一个由数据库对象,它只需要在初始化的过程中被访问:@dataclass class C: i: int j: int = None database: InitVar[DatabaseType] = Nonedef __post_init__(self,database): if self.j is None and database is not None: self.j = database.lookup('j')
c = C(10,database=my_database)
这个例子中会返回
c.i
和c.j
的数据,但是不会返回c.database
的。
dataclasses
模块中提供了一些常用函数供我们处理数据类。使用
dataclasses.asdict
和dataclasses.astuple
我们可以把数据类实例中的数据转换成字典或者元组:>>> from dataclasses import asdict,astuple >>> asdict(Lang()) {'name': 'python','strong_type': True,'static_type': False,'age': 28} >>> astuple(Lang()) ('python',28)使用
dataclasses.is_dataclass
可以判断一个类或实例对象是否是数据类:>>> from dataclasses import is_dataclass >>> is_dataclass(Lang) True >>> is_dataclass(Lang()) True
python3.7引入dataclass的一大原因就在于相比namedtuple,dataclass可以享受继承带来的便利。
dataclass
装饰器会检查当前class的所有基类,如果发现一个dataclass,就会把它的字段按顺序添加进当前的class,随后再处理当前class的field。所有生成的方法也将按照这一过程处理,因此如果子类中的field与基类同名,那么子类将会无条件覆盖基类。子类将会根据所有的field重新生成一个初始化函数,并在其中初始化基类。看个例子:
@dataclass class Python(Lang): tab_size: int = 4 is_script: bool = TruePython()
Python(name='python',age=28,tab_size=4,is_script=True)@dataclass
class Base:
x: float = 25.0
y: int = 0@dataclass
class C(Base):
z: int = 10
x: int = 15C()
C(x=15,y=0,z=10)
Lang
的field被Python
继承了,而C
中的x
则覆盖了Base
中的定义。没错,数据类的继承就是这么简单。
合理使用dataclass将会大大减轻开发中的负担,将我们从大量的重复劳动中解放出来,这既是dataclass的魅力,不过魅力的背后也总是有陷阱相伴,最后我想提几点注意事项:
- dataclass通常情况下是unhashable的,因为默认生成的
__hash__
是None
,所以不能用来做字典的key,如果有这种需求,那么应该指定你的数据类为frozen dataclass - 小心当你定义了和
dataclass
生成的同名方法时会引发的问题 - 当使用可变类型(如list)时,应该考虑使用
field
的default_factory
- 数据类的属性都是公开的,如果你有属性只需要初始化时使用而不需要在其他时候被访问,请使用
dataclasses.InitVar
只要避开这些陷阱,dataclass一定能成为提高生产力的利器。