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 "" , line 1, in
File "" , 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