当前位置: 移动技术网 > IT编程>脚本编程>Python > Python3.7 dataclass使用指南小结

Python3.7 dataclass使用指南小结

2019年03月19日  | 移动技术网IT编程  | 我要评论

例假不能吃什么,黄巢山漂流,尼达维里尔

dataclass简介

dataclass的定义位于pep-557,根据定义一个dataclass是指“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为dataclass,再通俗点讲,dataclass就是一个含有数据及操作数据方法的容器。

乍一看可能会觉得这个概念不就是普通的class么,然而还是有几处不同:

1.相比普通class,dataclass通常不包含私有属性,数据可以直接访问
2.dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值
3.dataclass拥有__eq__和__hash__魔法方法
4.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 = 0

  def total_cost(self) -> float:
    return self.unit_price * self.quantity_on_hand

同时使用dataclass也有一些好处,它比namedtuple更灵活。同时因为它是一个常规的类,所以你可以享受继承带来的便利。

dataclass的使用

我们分x步介绍dataclass的使用,首先是如何定义一个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, 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这个装饰器中,如果想要完全掌控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。

数据类的基石——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

dataclass继承

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的魅力,不过魅力的背后也总是有陷阱相伴,最后我想提几点注意事项:

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

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

参考

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网