4 道练习题·预计 80 分钟·做对一题解锁下一段
写代码这事,一不留神就会陷入「样板代码」的泥潭。各位先别急着反驳,咱们做个小实验:现在请你写一个 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))
e = Employee('两点水', '做鸭事业部', 1001, '2020-03-15', 12000)
print(e)输出:
Employee(name='两点水', dept='做鸭事业部', emp_id=1001, hire_date='2020-03-15', salary=12000)各位数一下,光这么一个普普通通的「数据类」,就花了二十多行。__init__ 写一遍字段,__repr__ 写一遍字段,__eq__ 写一遍字段,__hash__ 又写一遍字段——同一组字段名重复出现了五次。再想象一下你这个类有 15 个字段,那 __init__ 的参数列表就要排成一列火车,每个 self.xxx = xxx 都要复制粘贴,写到第十个就开始想骂人。
这就是所谓的「样板代码」。它没创造任何业务价值,纯粹是 Python 语法要求你必须这么写。写代码的人讨厌它,看代码的人也讨厌它,因为信息密度太低,真正重要的「这个类有哪些字段」被淹没在 self.xxx = xxx 的重复噪声里。
那有没有什么办法,能让我们只声明字段,剩下的活儿让 Python 自己干?
有。Python 3.7 给我们送来了 dataclass。从 3.10 起又给它加了 slots、kw_only 等更现代的开关。再往后还有第三方的 Pydantic,主打「带运行时校验的数据模型」。这一篇,咱们就把「轻量级数据类」这条线从 dataclass 一路捋到 Pydantic,让各位写数据结构的时候,再也不用手指头打结。
把上面那个 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 装饰器定义一个 Point 类:
x: int、y: intPoint(1, 2) 赋给变量 p,并 print(p)输出应该是:
Point(x=1, y=2)(这正是 dataclass 自动生成的 __repr__ 输出。)
推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai