Python3.7 dataclass使用指南小结


Posted in Python onFebruary 22, 2019

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一定能成为提高生产力的利器。

参考

https://docs.python.org/3.7/library/dataclasses.html

https://www.python.org/dev/peps/pep-0557

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python模块学习 filecmp 文件比较
Aug 27 Python
Python装饰器的函数式编程详解
Feb 27 Python
Python随机生成数模块random使用实例
Apr 13 Python
Python中将字典转换为XML以及相关的命名空间解析
Oct 15 Python
详解使用python crontab设置linux定时任务
Dec 08 Python
Python温度转换实例分析
Jan 17 Python
浅谈numpy生成数组的零值问题
Nov 12 Python
python按键按住不放持续响应的实例代码
Jul 17 Python
利用pyecharts实现地图可视化的例子
Aug 12 Python
PyTorch中常用的激活函数的方法示例
Aug 20 Python
python 函数嵌套及多函数共同运行知识点讲解
Mar 03 Python
使用python实现名片管理系统
Jun 18 Python
Python文件读写常见用法总结
Feb 22 #Python
Python 通过requests实现腾讯新闻抓取爬虫的方法
Feb 22 #Python
解决python3.5 正常安装 却不能直接使用Tkinter包的问题
Feb 22 #Python
浅谈python3.6的tkinter运行问题
Feb 22 #Python
Scrapy框架爬取西刺代理网免费高匿代理的实现代码
Feb 22 #Python
在Python运行时动态查看进程内部信息的方法
Feb 22 #Python
Python开启线程,在函数中开线程的实例
Feb 22 #Python
You might like
腾讯QQ微博API接口获取微博内容
2013/10/30 PHP
php选择排序法实现数组排序实例分析
2015/02/16 PHP
PHP创建文件,并向文件中写入数据,覆盖,追加的实现代码
2016/03/25 PHP
YII框架批量插入数据的方法
2017/03/18 PHP
PHP策略模式定义与用法示例
2017/07/27 PHP
thinkPHP5实现数据库添加内容的方法
2017/10/25 PHP
基于php中echo用逗号和用点号的区别详解
2018/01/23 PHP
JavaScript 入门·JavaScript 具有全范围的运算符
2007/10/01 Javascript
js弹出窗口之弹出层的小例子
2013/06/17 Javascript
基于JS实现的笛卡尔乘积之商品发布
2016/05/13 Javascript
JavaScript中省略元素对数组长度的影响
2016/10/26 Javascript
AngularJS递归指令实现Tree View效果示例
2016/11/07 Javascript
浅谈React Native 中组件的生命周期
2017/09/08 Javascript
基于vue2的canvas时钟倒计时组件步骤解析
2018/11/05 Javascript
了解JavaScript中let语句
2019/05/30 Javascript
vue router 传参获取不到的解决方式
2019/11/13 Javascript
使用Python脚本对Linux服务器进行监控的教程
2015/04/02 Python
python实现在字符串中查找子字符串的方法
2015/07/11 Python
简单谈谈Python流程控制语句
2016/12/04 Python
python3爬取淘宝信息代码分析
2018/02/10 Python
Python FTP两个文件夹间的同步实例代码
2018/05/25 Python
Python读取txt内容写入xls格式excel中的方法
2018/10/11 Python
python 缺失值处理的方法(Imputation)
2019/07/02 Python
Python pip install如何修改默认下载路径
2020/04/29 Python
在tensorflow实现直接读取网络的参数(weight and bias)的值
2020/06/24 Python
python开发入门——列表生成式
2020/09/03 Python
Python如何使用ElementTree解析xml
2020/10/12 Python
伦敦一卡通:The London Pass
2018/11/30 全球购物
英国知名小木屋定制网站:Tiger Sheds
2020/03/06 全球购物
2015年医药代表工作总结
2015/04/25 职场文书
人与自然的观后感
2015/06/18 职场文书
如何使JavaScript休眠或等待
2021/04/27 Javascript
Python爬取用户观影数据并分析用户与电影之间的隐藏信息!
2021/06/29 Python
Python编程源码报错解决方法总结经验分享
2021/10/05 Python
Java 关于String字符串原理上的问题
2022/04/07 Java/Android
Redis过期数据是否会被立马删除
2022/07/23 Redis