Python3.7中的Dataclass

Dataclass是Python3.7新增的对象类型,如果你还没有使用Python3.7——这是最新发布的Python版本,请尽快到官方网站下载安装,一边随本文一起体会它的新发展。 简介

Dataclass是Python的类,但适合存储数据对象。数据对象是什么?下面列出这种对象类型的几项特征,虽然不全面:

  • 它们存储数据并表示某种数据类型,例如:数字。对于熟悉ORM的朋友来说(如果不熟悉,请参阅《跟老齐学Python:Django实战》中的讲述),数据模型实例就是一个数据对象。它代表了一种特定的实体。它所具有的属性定义或表示了该实体。
  • 它们可以与同一类型的其他对象进行比较。例如:大于、小于或等于。

当然还有更多的特性,但是这里列出的足以帮助你理解“数据对象”关键所在。

为了理解Dataclass类,我们将写一个简单的类,它包含一个数字,并且允许我们执行上面提到的操作。

Python 3.7提供了一个装饰器dataclass,用它可以将一个类转换为“数据对象”的类,即dataclass。

from dataclasses import dataclass

@dataclass
class A:
    pass
复制代码

下面,对这种类型进行深入研究。

初始化

下面创建一个类,注意初始化方法,主要是实现以该对象属性存储数字。

>>> class Number:
...     def __init__(self, val):
...         self.val = val
...
>>> one = Number(1)
>>> one.val
1
复制代码

下面使用@dataclass装饰器实现以上同样的功能:

>>> @dataclass
... class Number:
...     val:int
...
>>> done = Number(1)
>>> done.val
1
复制代码

归纳使用dataclass装饰器带来的变化:

1.不需要在__init__方法中给实例self的属性赋值。

2.有了类型提示,提高了可读性。现在我们立即知道属性val是整数型的——似乎在吸收强类型语言的特征。

如果还记得《Python之禅》中说过的“Readability counts”(重在可读性),似乎感觉上面的做法吻合了这种要求。

还可以这样写,设置默认值。

>>> @dataclass
... class Number:
...     val:int = 0
...
>>> do = Number()
>>> do.val
0
>>> dt = Number(2)
>>> dt.val
2
复制代码

对象说明的表示方式

对象说明应该是用有意义的字符串表示的,它在在程序调试中非常有用。

默认的Python对象表示法不是很有意义:

>>> class Number:
...     def __init__(self, val=0):
...         self.val = val
...
>>> a = Number()
>>> a
<__main__.Number object at 0x10279bf60>
复制代码

返回值没有用有意义的字符串表示,我们仅仅能够知道它在内存中的地址。

所以,如果你要自定义对象类型,最好要使用__repr__方法,它是一个对解释器友好的方法(详见《跟老齐学Python:轻松入门》中的有关阐述),通过这个方法可以对此对象给予有意义的说明。

>>> class Number:
...     def __init__(self, val=0):
...         self.val = val
...     def __repr__(self):
...         return "your object is: " + str(self.val)
...
>>> a = Number()
>>> a
your object is: 0
复制代码

再来看调试结果,则告诉我们,变量a引用的对象就是数字1——我们得到一个有意义的对象说明:

如果不用上面的方式,而是使用@dataclass装饰器定义一个Dataclass类型的对象,则会自动添加一个__repr__函数,这样我们就不必手动实现它。

>>> @dataclass
... class Number:
...     val:int = 0
...
>>> a = Number()
>>> a
Number(val=0)
复制代码

数据比较

一般来说,数据对象需要相互比较。

两个变量a和b所引用的对象,可以进行如下各种比较操作:

a < b
a > b
a == b
a >= b
a <= b
复制代码

在Python中,如果自定义对象类型实现上述各项比较功能,就必须定义相应的方法,比如要实现“==”和“<”比较,就要分别实现“__eq__”和“__lt__”两个特殊方法(详见《跟老齐学Python:轻松入门》中的说明)。下面是一个简单的例子:

class Number:
    def __init__( self, val = 0):
        self.val = val
 
    def __eq__(self, other):
        return self.val == other.val
 
    def __lt__(self, other):
        return self.val < other.val
复制代码

这是一种通常的自定义对象类型,并该类型对象能够实现比较“==”和“<”的比较运算。

如果使用@dataclass,则让代码简单了许多。

@dataclass(order = True)

class Number:
    val: int = 0
复制代码

就这么多代码,惊讶了吧。

不需要定义__eq____lt__方法。只需要在@dataclass装饰器的参数中设置order = True即可,就自动在所定义的类中实现了这两个特殊方法。

那么,怎么做到的呢?

当你使用@dataclass时,它会向类定义中添加函数__eq____lt__。这是我们已经知道的。那么,这些函数是如何知道检查是否相等和进行比较的呢?

一个由dataclass生成的__eq__函数将把属性元组与相同类的另一个实例的属性的元组进行比较。在我们的例子中,由__eq__方法自动生成的对象将等同于:

def __eq__(self, other):
    return (self.val,) == (other.val,)
复制代码

让我们来看一个更详细的例子:

写一个数据类Person来保存姓名和年龄。

@dataclass(order = True)
class Person:
    name: str
    age:int = 0
复制代码

这个自动生成的__eq__方法将等价于:

def __eq__(self, other):
    return (self.name, self.age) == ( other.name, other.age)
复制代码

注意属性的顺序。它们总是按照你在数据类中定义的顺序生成的。

同理,等效的__le__函数将类似于这样:

def __le__(self, other):
    return (self.name, self.age) <= (other.name, other.age)
复制代码

因为数据对象中已经默认实现了各种比较功能,因此就可以实现排序。

>>> import random
>>> a = [Number(random.randint(1,10)) for _ in range(10)] #随机数列表
>>> a
>>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)]
>>> sorted_a = sorted(a) #Sort Numbers in ascending order
>>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)]
>>> reverse_sorted_a = sorted(a, reverse = True) #降序排列 
>>> reverse_sorted_a
>>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]
复制代码

创建不可变对象

在Python中,自定义不可变对象,其实有点麻烦。

>>> class Number:
...     val: int = 0
...
>>> a = Number()
>>> a.val
0
>>> a.val = 10    #所谓不变对象,就是当用这种方式修改属性值的时候,应该不允许,应该报异常
>>> a.val         #当然,在这里没有报异常,而是实现了修改,因为此处的对象不是不可变的,是可变的。
10
复制代码

在Python3.7中,有了dataclass,则可以很轻松的实现上述设想。

在下面的代码中,使用了@dataclass装饰器,实现不可变对象。

>>> from dataclasses import dataclass

>>> @dataclass(frozen=True)
... class Book:
...     name: str = "Learn Python with Laoqi"
...
>>> python_book = Book()
>>> python_book.name
'Learn Python with Laoqi'

>>> python_book.name = "other Python Book"    #试图进行修改,结果报异常,不允许修改
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'
复制代码

用这种方式设置常熟,是不是更优雅。

使用__post_init__进行初始化后处理

什么是初始化后处理?下面举一个例子,说明之:

>>> import math
>>> class Float:
...     def __init__(self, val=0):
...         self.val = val
...         self.process()
...     def process(self):
...         self.decimal, self.integer = math.modf(self.val)
...
>>> a = Float(2.5)
>>> a.decimal
0.5
>>> a.integer
2.0
复制代码

在类Float中,定义了方法process,并且在初始化方法__init__中调用,这就是要在初始化之后做的事情。

在Python3.7中,具有上述功能代码可以使用__post_init__来完成。如下所示:

>>> @dataclass
... class FloatNumber:
...     val: float = 0.0
...     def __post_init__(self):
...         self.decimal, self.integer = math.modf(self.val)
...
>>> b = FloatNumber(2.5)
>>> b.val
2.5
>>> b.integer
2.0
>>> b.decimal
0.5
复制代码

是不是再次感受到了“代码的整洁”。


参考资料:

https://medium.com/mindorks/understanding-python-dataclasses-part-1-c3ccd4355c34
https://docs.python.org/3/library/dataclasses.html
复制代码

  • 《跟老齐学Python:轻松入门》:面向初学Python的读物,深入浅出地讲解Python3基础知识
  • 《跟老齐学Python:Django实战》:是Python在网站开发方面的书籍,以项目的方式介绍Django框架的应用方式
  • 《跟老齐学Python:数据分析》:是Python在数据分析、机器学习方面的基础读物,重点介绍Numpy、Pandas的有关知识和数据可视化的实现方法。

以上书籍,各大网店有售。相关网站:itdiffer.com

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值