4 道练习题·预计 35 分钟·做对一题解锁下一段
写代码这事,一不留神就会陷入「样板代码」的泥潭。先做个小实验:现在请你写一个 Employee 类,要求是这样的——存员工的姓名、部门、工号、入职日期、月薪,要能正常打印(不能是 <__main__.Employee object at 0x...> 这种鬼东西),要能比较两个员工对象是否相等,最好还能拿出来做字典的 key。
听起来不难是吧?大概长这样:
class Employee:
def __init__(self, name, dept, emp_id, hire_date, salary):
self.name = name
self.dept = dept
self.emp_id = emp_id
self.hire_date = hire_date
self.salary = salary
def __repr__(self):
return (
f'Employee(name={self.name!r}, dept={self.dept!r}, '
f'emp_id={self.emp_id!r}, hire_date={self.hire_date!r}, '
f'salary={self.salary!r})'
)
def __eq__(self, other):
if not isinstance(other, Employee):
return NotImplemented
return (
self.name == other.name
and self.dept == other.dept
and self.emp_id == other.emp_id
and self.hire_date == other.hire_date
and self.salary == other.salary
)
def __hash__(self):
return hash((self.name, self.dept, self.emp_id, self.hire_date, self.salary))数一下,光这么一个普普通通的「数据类」,就花了二十多行。__init__ 写一遍字段,__repr__ 写一遍字段,__eq__ 写一遍字段,__hash__ 又写一遍字段——同一组字段名重复出现了五次。再想象一下你这个类有 15 个字段,那 __init__ 的参数列表就要排成一列火车,每个 self.xxx = xxx 都要复制粘贴,写到第十个就开始想骂人。
它没创造任何业务价值,纯粹是 Python 语法要求你必须这么写。写代码的人讨厌它,看代码的人也讨厌它,因为信息密度太低,真正重要的「这个类有哪些字段」被淹没在 self.xxx = xxx 的重复噪声里。
那有没有什么办法,能让我们只声明字段,剩下的活儿让 Python 自己干?
有。Python 3.7 给我们送来了 dataclass。从 3.10 起又给它加了 slots、kw_only 等更现代的开关。这一节,我们就把 dataclass 这条线从基础用法一路捋到现代写法,让各位写数据结构的时候,再也不用手指头打结。
@dataclass 三行代码搞定 __init__ / __repr__ / __eq__field(default_factory=...) 处理可变默认值__eq__ 自动按字段值比较学完之后,再写数据类基本上就是「贴一个装饰器、列一下字段」这种轻松活儿。
把上面那个 Employee 类,用 dataclass 重写一遍:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
emp_id: int
hire_date: str
salary: int
e = Employee('两点水', '研发部', 1001, '2020-03-15', 12000)
print(e)输出:
Employee(name='两点水', dept='研发部', emp_id=1001, hire_date='2020-03-15', salary=12000)
二十多行的代码,缩成了不到十行。@dataclass 这个装饰器一贴,Python 就帮我们干了这些事:
name: str、dept: str 这些「带类型注解的类变量」,自动当成字段__init__,参数顺序就是字段顺序__repr__,长得跟咱们手写的那种「类名(字段=值, 字段=值)」一模一样__eq__,按字段逐个比较整个过程,你只需要把字段名和它的类型写出来,剩下的全是 dataclass 在帮你干活。
那「类型注解」是不是必须的?是的。这是 dataclass 识别字段的依据。你如果只写 name = '' 而不写 name: str,dataclass 就认不出来——它会把 name 当成一个普通的类属性,不会进 __init__ 的参数列表。
记住一句话:在 dataclass 里,name: str 是「字段声明」,name = '默认值' 是「类属性」,两者作用截然不同。
from dataclasses import dataclass
@dataclass
class Demo:
a: int # 这是字段,会进 __init__
b: int = 10 # 这是有默认值的字段,也会进 __init__
c = 20 # 注意:这里没有类型注解,被当成普通类属性,不会进 __init__
d = Demo(1)
print(d)
print(d.c)输出:
Demo(a=1, b=10)
20
看到没?c 没出现在 repr 里,因为它根本不是一个字段,只是个挂在类上的常量。
dataclass 默认会生成 __eq__,所以两个字段值完全相同的对象,会被判为相等:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
a = Employee('两点水', '研发部', 12000)
b = Employee('两点水', '研发部', 12000)
c = Employee('两点水', '研发部', 15000)
print(a == b)
print(a == c)
print(a is b)输出:
True
False
False
注意第三行——a is b 是 False。== 比的是「字段值是否相等」,is 比的是「是不是同一个对象」。这两个事完全两码事,别混。
请用 @dataclass 装饰器定义一个 Point 类:
x: int、y: intPoint(1, 2) 赋给变量 p,并 print(p)输出应该是:
Point(x=1, y=2)
(这正是 dataclass 自动生成的 __repr__ 输出。)