Python 增强提案PEP

  1. Python中10个必读的PEP提案
  2. 理解Python数据类:Dataclass 的特征概述 (上)
  3. 理解Python数据类:Dataclass fields 的概述(下)
  4. Python3.7 dataclass使用指南小结

​ PEP 是 Python 增强提案(Python Enhancement Proposal)的缩写。社区通过PEP来给 Python 语言建言献策,每个版本你所看到的新特性和一些变化都是通过PEP提案经过社区决策层讨论、投票决议的。

PEP 557 数据类(data class)

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

Dataclasses 是一些适合于存储数据对象(data object)的 Python 类。
下面是一个并不详尽的用于定义数据对象的特征列表:
• 他们存储并表示特定的数据类型。例如:一个数字。对于那些熟悉对象关系映射(Object Relational Mapping,简称 ORM)的人来说,一个模型实例就是一个数据对象。它表示了一种特定类型的实体。它存储了用于定义或表示那种实体的属性。
• 他们能够被用于和同类型的其他对象进行比较。例如,一个数字可能大于,小于或等于另一个数字。
• 当然数据对象还有更多的特征,但上述内容足以帮助你理解关键部分。
• 为了理解 Dataclases,我们将实现一个简单的类。它能够存储一个数字,并允许我们执行上面提到的各种运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
1.相比普通class,dataclass通常不包含私有属性,数据可以直接访问
2.dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值
3.dataclass拥有__eq__和__hash__魔法方法
4.dataclass有着模式单一固定的构造方式,或是需要重载运算符,而普通class通常无需这些工作

# -*- coding: utf-8 -*
import random
from typing import List
from dataclasses import dataclass
from dataclasses import field

# dataclasses.field 接受了一个名为 default_factory 的参数,它的作用是:如果在创建对象时没有赋值,则使用该方法初始化该字段。
# default_factory 必须是一个可以调用的无参数方法(通常为一个函数)
def get_random_marks():
return [random.randint(1, 10) for _ in range(5)]

@dataclass(order=True)
class Student:
marks: List[int] = field(default_factory=get_random_marks)
name: str = field(default='noname', compare=False)
age: int = field(default=18, repr=False)
# 在追踪一个对象的状态,并且希望它在初始化时一直被设为False
# 更一般地,这个值在初始化时不能够被传递,init决定是否生成init
verified: bool = field(repr=False, init=False, default=False)

# 不好的方式:自定义比较方法
# 正确方式:
# dataclass能够自动生成 <, =, >, <= 和 >= 这些比较方法。但是这些比较方法的一个缺陷是,
# 它们使用类中的所有字段进行比较, 并且是按定义顺序
# 这里使用(order=True),把不需要的字段定义为 filed(compare=False)
# def __eq__(self, other):
# return (self.marks, self.name) == (other.marks, other.name)

# 不好的方法:初始化一个变量为列表, 使用__post_init__方法, 或参数传递
# 正确方式是使用field
# def __post_init__(self):
# self.marks = get_random_marks()

# 不要自己定义,会自动生成的__repr__方法使用所有的字段用于表示
# 不需要的元素使用field(repr=False)过滤
# def __repr__(self):
# return self.name+' '+' '.join(self.marks)

student_1 = Student([random.randint(1, 10) for _ in range(5)], 'tom', 17)
student_2 = Student(age=16, name='Rick')
# 这里会报异常
# student_3 = Student(verified=True)
print(student_1)
print(f'student_1 == student_2: {student_1 == student_2}')

# 使用dataclasses.asdict和dataclasses.astuple我们可以把数据类实例中的数据转换成字典或者元组:
from dataclasses import asdict, astuple
print(asdict(student_2))
# 使用dataclasses.is_dataclass可以判断一个类或实例对象是否是数据类:
print(f'isinstance(student_1, Student): {isinstance(student_1, Student)}')

>>>
Student(marks=[7, 6, 7, 2, 3], name='tom')
student_1 == student_2: False
{'marks': [6, 4, 3, 5, 1], 'name': 'Rick', 'age': 16, 'verified': False}
isinstance(student_1, Student): True
1
2
3
装饰器的原型如下:
dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
dataclass装饰器将根据类属性生成数据类和数据类需要的方法
key 含义
init 是否自动生成init,如果已经有定义同名方法则忽略这个值,也就是指定为True也不会自动生成
repr 是否自动生成repr;自动生成的打印格式为class_name(arrt1:value1, attr2:value2, …)
eq 是否生成eq;按属性在类内定义时的顺序逐个比较,全部的值相同才会返回True
order 自动生成ltlegtge,比较方式与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赋值将会引发错误,对象将是不可变的,如果已经定义了setattrdelattr将会引发TypeError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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返回的这个对象。
init参数如果设置为False,表示不为这个field生成初始化操作,dataclass提供了hook—— __post_init__供我们利用这一特性:
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)

dataclass继承
python3.7引入dataclass的一大原因就在于相比namedtuple,dataclass可以享受继承带来的便利。
dataclass装饰器会检查当前class的所有基类,如果发现一个dataclass,就会把它的字段按顺序添加进当前的class,随后再处理当前class的field。所有生成的方法也将按照这一过程处理,因此如果子类中的field与基类同名,那么子类将会无条件覆盖基类。子类将会根据所有的field重新生成一个构造函数,并在其中初始化基类。

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