本文将带你走进python3.7的新特性dataclass,经过本文你将学会dataclass的使用并避免踏入某些陷阱。html
dataclass的定义位于PEP-557,根据定义一个dataclass是指“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性都可公开访问,能够带有默认值并能被修改,并且类中含有与这些属性相关的类方法,那么这个类就能够称为dataclass,再通俗点讲,dataclass就是一个含有数据及操做数据方法的容器。python
乍一看可能会以为这个概念不就是普通的class么,然而仍是有几处不一样:数据库
__eq__
和__hash__
魔法方法基于上述缘由,一般本身实现一个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 = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand
同时使用dataclass也有一些好处,它比namedtuple更灵活。同时由于它是一个常规的类,因此你能够享受继承带来的便利。post
咱们分x步介绍dataclass的使用,首先是如何定义一个dataclass。code
dataclasses
模块提供了一个装饰器帮助咱们定义本身的数据类:htm
@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, False, 23) Lang(name='js', strong_type=False, static_type=False, age=23) >>> Lang('js', False, False, 23) == Lang() False >>> Lang('python', True, False, 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
上:
key | 含义 |
---|---|
init | 指定是否自动生成__init__ ,若是已经有定义同名方法则忽略这个值,也就是指定为True也不会自动生成 |
repr | 同init,指定是否自动生成__repr__ ;自动生成的打印格式为class_name(arrt1:value1, attr2:value2, ...) |
eq | 同init,指定是否生成__eq__ ;自动生成的方法将按属性在类内定义时的顺序逐个比较,所有的值相同才会返回True |
order | 自动生成__lt__ ,__le__ ,__gt__ ,__ge__ ,比较方式与eq相同;若是order指定为True而eq指定为False,将引起ValueError ;若是已经定义同名函数,将引起TypeError |
unsafehash | 若是是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)当设置为True时将会根据类属性自动生成 __hash__ ,然而这是不安全的,由于这些属性是默承认变的,这会致使hash的不一致,因此除非能保证对象属性不可随意改变,不然应该谨慎地设置该参数为True |
frozen | 设为True时对field赋值将会引起错误,对象将是不可变的,若是已经定义了__setattr__ 和__delattr__ 将会引起TypeError |
有默认值的属性必须定义在没有默认值的属性以后,和对kw参数的要求同样。
上面咱们偶尔提到了field的概念,咱们所说的数据类属性,数据属性实际上都是被field的对象,它表明着一个数据的实体和它的元信息,下面咱们了解一下dataclasses.field
。
先看下field的原型:
dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, 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, 2, 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
__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] = None def __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', True, False, 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 = True >>> Python() Python(name='python', strong_type=True, static_type=False, 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 = 15 >>> C() C(x=15, y=0, z=10)
Lang
的field被Python
继承了,而C
中的x
则覆盖了Base
中的定义。
没错,数据类的继承就是这么简单。
合理使用dataclass将会大大减轻开发中的负担,将咱们从大量的重复劳动中解放出来,这既是dataclass的魅力,不过魅力的背后也老是有陷阱相伴,最后我想提几点注意事项:
__hash__
是None
,因此不能用来作字典的key,若是有这种需求,那么应该指定你的数据类为frozen dataclassdataclass
生成的同名方法时会引起的问题field
的default_factory
dataclasses.InitVar
只要避开这些陷阱,dataclass必定能成为提升生产力的利器。