一文搞懂dataclass和field

目录

  • 1. 背景
  • 2. dataclass函数签名详解
    • 2.1 repr示例
    • 2.2 eq与order示例
    • 2.3 frozen示例
    • 2.4 `__post_init__`
    • 2.5 继承
  • 3. Field
    • 3.1 default与default_factory
    • 3.2 init与repr
    • 3.3 compare
    • 3.4 metadata
  • Ref

1. 背景

考虑这样一个场景。假如我们要定义一个 Person 类,并希望它具有姓名、性别、年龄、身高、体重这五个属性,则可以这样写:

class Person:def __init__(self, name: str, age: int, sex: str, height: float, weight: float):self.name = nameself.age = ageself.sex = sexself.height = heightself.weight = weight

如果我们还希望 print(person) 的时候能够打印出这个人的所有信息,并且支持比较两个人是否相等,则应当添加 __repr____eq__ 方法:

class Person:def __init__(self, name: str, age: int, sex: str, height: float, weight: float):self.name = nameself.age = ageself.sex = sexself.height = heightself.weight = weightdef __repr__(self):return (f"Person(name={self.name!r}, age={self.age}, sex={self.sex!r}, "f"height={self.height}, weight={self.weight})")def __eq__(self, other):if isinstance(other, Person):return (self.name == other.name and self.age == other.age andself.sex == other.sex and self.height == other.height andself.weight == other.weight)return False

可以看出代码有些许复杂,如果使用 dataclass,代码将得到大大简化:

from dataclasses import dataclass@dataclass
class Person:name: strage: intsex: strheight: floatweight: float

在使用了 @dataclass 装饰器后,我们只需要声明每个属性及对应的类型,dataclass 会自动为该类生成 __init____repr____eq__ 方法,生成的 __eq__ 方法会比较所有的属性

有了以上背景知识后,我们再来看一下 dataclasses 这个库到底是干什么的:

📝 dataclasses 是 Python 3.7 及更高版本中引入的一个标准库,用于简化类的定义。它旨在通过使用装饰器和类型注解来减少样板代码,特别是在创建一个用于存储数据的类的情况下。dataclasses 提供了一个装饰器和一系列支持函数,让你能够快速定义保存数据的类,同时自动添加特殊方法,比如 __init__()__repr__()__eq__() 等。

2. dataclass函数签名详解

dataclass 的函数签名如下:

def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,unsafe_hash=False, frozen=False, match_args=True,kw_only=False, slots=False, weakref_slot=False):

⚠️ / 之前的所有参数,都必须以位置形式传参,不得以关键字形式传参。* 之后的所有参数,都必须以关键字形式传参,不得以位置形式传参。/* 之间的所有参数,既可以以位置形式传参,也可以以关键字形式传参。该特性由Python 3.8版本引入。

cls 是将要被装饰的类。

  • init:决定是否为类添加 __init__ 方法。
  • repr:决定是否为类添加 __repr__ 方法。
  • eq:决定是否为类添加 __eq__ 方法。
  • order:决定是否为类添加 __lt____le____gt____ge__ 方法。
  • frozen:决定是否冻结实例化后的属性。一旦冻结,任何企图修改对象属性的行为都会引发 FrozenInstanceError

⚠️ dataclasscls 添加方法时实际上是通过 setattr 函数来实现(例如 setattr(cls, "__repr__", _repr_fn))。在 setattr(object, name, value) 中,object 可以是类对象,实例对象,模块对象,或是实现了 __setattr__ 方法的对象。name 只能是字符串。value 可以是基本数据类型,容器类型,类的实例,函数等。

2.1 repr示例

@dataclass
class Person:name: strage: intsex: strheight: floatweight: floatperson = Person(name="John Doe", age=30, sex="Male", height=1.75, weight=70)print(person)
# Person(name='John Doe', age=30, sex='Male', height=1.75, weight=70)

可以看到 person 的所有信息都被打印出来了,并且打印的格式和实例化的格式几乎相同。

⚠️ name: str 仅仅是一个类型注解,我们完全可以在实例化的时候向 name 传入一个整数。

我们还可以预设默认值:

@dataclass
class Person:name: str = "John Doe"age: int = 30sex: str = "Male"height: float = 1.75weight: float = 70person = Person()print(person)
# 结果同上

注意,类型注解不是必须的,但此时必须要给相应的变量赋值

@dataclass
class Person:name = "John Doe"age: intsex: strheight = 1.75weight  # 这一行会报错

2.2 eq与order示例

注意到 eqorder 的一个简化情形,这里我们只讨论 orderdataclass 是如何比较两个类的大小呢?

事实上,在比较类的大小时,dataclass 会将类的所有属性按照其声明顺序打包成一个元组,然后比较两个元组的大小。以上述的 Person 为例,当设置 order=True 时,其生成的 __le__ 方法等价于:

def __le__(self, other):return ((self.name, self.age, self.sex, self.height, self.weight) <=(other.name, other.age, other.sex, other.height, other.weight))

由于 name 是字符串,则会按照字典序进行比较,如果 self.name <= other.name,则 self <= other。如果 self.name == other.name,那么就会比较下一项 age,以此类推。

一些比较的例子:

@dataclass(order=True)
class Person:name: strage: intsex: strheight: floatweight: floatp1 = Person("Alice", 30, "Female", 1.65, 60)
p2 = Person("Bob", 25, "Male", 1.75, 80)
p3 = Person("Alice", 35, "Female", 1.65, 60)
p4 = Person("Alice", 30, "Female", 1.70, 65)
p5 = Person("Charlie", 30, "Male", 1.80, 90)
p6 = Person("Bob", 25, "Male", 1.75, 75)
p7 = Person("Alice", 30, "Male", 1.65, 60)
p8 = Person("Alice", 30, "Female", 1.65, 55)comparisons = [(p1, p2),(p1, p3),(p1, p4),(p1, p5),(p2, p6),(p1, p7),(p1, p8),
]results = [(p1 <= p2) for p1, p2 in comparisons]
print(results)
# [True, True, True, True, False, True, False]

2.3 frozen示例

dataclass 中设置 frozen=True 会使得生成的类实例是不可变的。这意味着一旦一个实例被创建,它的任何属性都不能被改变。这类似于一个只读对象,可以确保实例在创建后保持不变,有助于确保数据的一致性和线程安全性。

from dataclasses import dataclass@dataclass(frozen=True)
class Person:name: strage: intsex: strheight: floatweight: floatimmutable_person = Person("Jane", 28, "Female", 1.68, 58)
immutable_person.age = 29
# dataclasses.FrozenInstanceError: cannot assign to field 'age'

2.4 __post_init__

__post_init__ 方法是 dataclasses 中一个非常有用的特性,允许你在一个类的初始化后立即运行一些额外的代码。当你使用 @dataclass 装饰器来装饰一个类时,这个类会自动获得一个 __init__ 方法,它会根据类中定义的属性来初始化实例。然而,如果你需要在实例化后立即执行一些操作(比如属性验证、转换、或基于其他属性计算一个属性的值),__post_init__ 方法就显得非常有用。

📝 __post_init__dataclasses 模块特有的一个方法,普通的类没有这个方法。

考虑这样一个场景,如果一个类只有两个属性 ab(均为 int 型),a 由人工输入决定,b 的值是 a 的值的两倍,如果使用普通类,我们可以这样写:

class MyClass:def __init__(self, a: int):self.a = aself.b = a * 2

如果使用 dataclass,我们的第一反应可能是下面这样:

@dataclass
class MyClass:a: intb: int = a * 2

但这样会出现报错:NameError: name 'a' is not defined,因为这种做法实际上相当于:

class MyClass:def __init__(self, a: int, b: int = a * 2):self.a = aself.b = b

我们可以添加 __post_init__ 方法,它会在 __init__ 之后立刻执行:

@dataclass
class MyClass:a: intdef __post_init__(self):self.b = self.a * 2myclass = MyClass(a=2)
print(myclass.a)
print(myclass.b)  # 4

除了计算属性之外, __post_init__ 还可以用来校验属性,如下是一个较为复杂的例子:

from dataclasses import dataclass, field
import redef validate_email(email):pattern = r"^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"if not re.match(pattern, email):raise ValueError(f"无效的电子邮件地址: {email}")def validate_phone(phone):pattern = r"^\+?[0-9]{10,15}$"if not re.match(pattern, phone):raise ValueError(f"无效的电话号码: {phone}")@dataclass
class Employee:name: strage: intemail: strphone: strdepartment: strsalary: floatdef __post_init__(self):if len(self.name) < 3:raise ValueError("姓名长度必须至少为3个字符")if not (18 <= self.age <= 65):raise ValueError("年龄必须在18到65之间")validate_email(self.email)validate_phone(self.phone)if len(self.department) == 0:raise ValueError("部门不能为空")if self.salary < 0:raise ValueError("薪资不能为负数")

2.5 继承

dataclass 同样支持继承,子类将拥有父类的所有属性和方法:

@dataclass
class Person:name: strage: intdef greet(self):return f"Hello, my name is {self.name} and I am {self.age} years old."@dataclass
class Employee(Person):salary: floatdef show_salary(self):return f"My salary is {self.salary}."emp = Employee(name="John Doe", age=30, salary=50000)print(emp.greet())
print(emp.show_salary())

子类还可以重写父类的属性和方法:

@dataclass
class Person:name: str = 'None'age: int = -1def introduce(self):return f"Hello, my name is {self.name} and I am {self.age} years old."@dataclass
class Employee(Person):salary: float = 50000name: str = 'John Doe'age: int = 30def introduce(self):base_introduction = super().introduce()return f"{base_introduction}\nMy salary is {self.salary}."emp = Employee()
print(emp.introduce())

3. Field

📝 被 @dataclass 装饰的类的属性又叫做字段(field)。

注意,以上的解释并不意味着一个被 @dataclass 装饰的类的属性就是 Field 的实例:

@dataclass
class MyClass:a: int = 2myclass = MyClass()
print(type(myclass.a))
# <class 'int'>

引入 Field 的目的是为了更灵活、更精细化地控制数据类的字段,Field 的构造函数如下:

class Field:def __init__(self, default, default_factory, init, repr, hash, compare, metadata):

3.1 default与default_factory

⚠️ defaultdefault_factory 不能同时指定。

default 参数用来指定字段的默认值。如果字段未在实例化时提供值,则会使用此默认值。

from dataclasses import dataclass, field@dataclass
class A:a: int = field(default=2)A1 = A()
A2 = A(3)
print(A1.a, A2.a)
# 2 3

如果要设置某一个字段的默认值为可变类型(例如列表、字典等),那么所有实例将共享这同一个默认值对象(参考博客)。这可能会导致意想不到的行为,因为修改任何一个实例的字段将影响所有实例。例如:

@dataclass
class A:a: List[int] = []

将会触发报错

ValueError: mutable default <class 'list'> for field a is not allowed: use default_factory

使用 default_factory 时,我们需要为它赋值一个无参数的可调用对象。每次创建数据类的实例时,都会调用这个可调用对象来生成该字段的默认值。面对以上报错,我们可以使用 default_factory 来解决:

@dataclass
class A:a: List[int] = field(default_factory=list)  # 默认值是空列表b: List[int] = field(default_factory=lambda: [1, 2, 3])  # 默认值是[1, 2, 3]

3.2 init与repr

init 参数用于指定一个字段是否应该包含在自动生成的 __init__ 方法中,请看下面的例子:

@dataclass
class A:a: intb: int = field(init=False)print(inspect.signature(A.__init__))
# (self, a: int) -> None

通过函数签名可以看出,A 自动生成的构造函数里,并没有 b 这个形参,如果我们执行 a = A(1, 2) 这样的实例化,则会报错:

TypeError: __init__() takes 2 positional arguments but 3 were given

设置 init=True 后的函数签名变成:(self, a: int, b: int) -> None

使用场景: 如果某个字段是基于其他字段的值计算得出的,我们可以将该字段的 init 参数设置为 False(非必须,参考2.4节,但为了可读性最好还是声明一下),然后在 __post_init__ 中计算。

repr 参数控制一个字段是否应该包含在自动生成的 __repr__ 方法的返回值中,请看下面的例子:

@dataclass
class A:a: intb: int@dataclass
class B:a: intb: int = field(repr=False)a = A(1, 2)
b = B(1, 2)print(a)  # A(a=1, b=2)
print(b)  # B(a=1)

使用场景: 如果类中某些字段是辅助性质的,或者可能包含大量数据,不适合在每次打印对象时都显示,则可以通过将这些字段的 repr 参数设置为 False 来排除它们。

3.3 compare

compare 用来决定是否在比较方法中(如 __eq__, __lt__, __le__, __gt__, 和 __ge__)包含该字段。

from dataclasses import dataclass, field@dataclass
class Book:title: strauthor: stryear: int = field(compare=False)book1 = Book("The Great Gatsby", "F. Scott Fitzgerald", 1925)
book2 = Book("The Great Gatsby", "F. Scott Fitzgerald", 2020)print(book1 == book2)  # True

3.4 metadata

metadata 参数用来为字段附加额外信息,这些信息不影响字段的行为,但可以被你的程序或第三方库用于各种目的,例如验证、序列化或其他自定义处理。

metadata 通常是一个字典:

@dataclass
class Product:name: strprice: float = field(default=0.0, metadata={'unit': 'USD'})in_stock: bool = field(default=True, metadata={'description': 'Whether the product is in stock'})

HuggingFace的 TrainingArguments 实际上就是典型的数据类,如下展示了它的前几个字段:

@dataclass
class TrainingArguments:framework = "pt"output_dir: str = field(metadata={"help": "The output directory where the model predictions and checkpoints will be written."},)overwrite_output_dir: bool = field(default=False,metadata={"help": ("Overwrite the content of the output directory. ""Use this to continue training if output_dir points to a checkpoint directory.")},)do_train: bool = field(default=False, metadata={"help": "Whether to run training."})do_eval: bool = field(default=False, metadata={"help": "Whether to run eval on the dev set."})do_predict: bool = field(default=False, metadata={"help": "Whether to run predictions on the test set."})

Ref

[1] https://zhuanlan.zhihu.com/p/59657729
[2] https://blog.csdn.net/be5yond/article/details/119545119
[3] https://zhuanlan.zhihu.com/p/61553610

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/746913.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FastAPI 是什么?深入解析

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建基于 Python 的 API。它是一个开源项目&#xff0c;基于 Starlette 和 Pydantic 库构建而成&#xff0c;提供了强大的功能和高效的性能。 FastAPI 官网地址&#xff1a;fastapi.tiango…

《软件工程》复试问答题总结

软件系统的三个测试阶段&#xff1a; 第一阶段&#xff1a;发现和解决BUG 集中在发现bug&#xff0c;考研测试设计能力&#xff0c;发现bug之后如何清晰表述定级&#xff0c;以及验证&#xff0c;之后举一反三尽早发现更多类似bug 第二阶段&#xff1a;质量的管理 多做质量数据…

Vue3自定义指令!!!

通过自定义指令实现菜单显示和权限控制问题。 一、新建一个在src目录下创建包directives&#xff0c;在包中创建一个ts文件。 import { useStore } from "/store/pinia";function hasRoles(role: any) {const pinaRoles useStore().roles;if (typeof role "s…

【RPG Maker MV 仿新仙剑 战斗场景UI (四)】

RPG Maker MV 仿新仙剑 战斗场景UI 四 三级战斗指令菜单效果代码完成效果 下篇预告 三级战斗指令菜单 仙剑1中三级战斗的菜单内容如下&#xff1a;使用、投掷、装备这三项。 效果 在RMMV中原始菜单中是没有这三级菜单的&#xff0c;因此需要重新进行添加进去。 代码 这里贴…

分布式思想

1、单体架构设计存在的问题 传统项目采用单体架构设计,虽然可以在一定的程度上解决企业问题,但是如果功能模块众多,并且将来需要二次开发.由于模块都是部署到同一台tomcat服务器中,如果其中某个模块代码出现了问题,将直接影响整个tomcat服务器运行. 这样的设计耦合性太高.不便…

19.ADC模数转换器知识点+AD单通道AD多通道应用程序示例

0. 江协科技/江科大-STM32标准库开发-各章节详细笔记-查阅传送门_江协科技stm32笔记-CSDN博客文章浏览阅读2.9k次&#xff0c;点赞44次&#xff0c;收藏128次。江协科技/江科大-STM32标准库开发-各章节详细笔记-传送门至各个章节笔记。基本上课程讲的每句都详细记录&#xff0c…

Python转C++的童鞋看这里

一、前言 Python学完了&#xff0c;很多人都去学了C。在学习C之前&#xff0c;建议大家先打好基础&#xff0c;对C和Python的区别先了解了解&#xff0c;会对后续的C学习提供很大的帮助。 二、特点区分 1. Python Python是一种简单而高效的语言&#xff0c;它已经帮你封装好了…

Python-OpenCV-边缘检测

摘要&#xff1a; 本文详细介绍了Python-OpenCV的边缘检测技术&#xff0c;包括基础知识回顾、功能实现、技巧与实践、常见问题与解答等&#xff0c;为读者提供了全面深入的教程。 阅读时长&#xff1a;约60分钟 关键词&#xff1a;Python, OpenCV, 边缘检测, Canny, Sobel …

群晖 Synology Photos DSM7 自定义文件夹管理照片

背景 众所周知&#xff0c;目前群晖DSM7中使用Synology Photos做照片管理时&#xff0c;个人照片只能默认索引 /home/Photos 文件夹&#xff0c;但是如果个人照片很多或者用户很多时&#xff0c;共享文件夹/homes 所在的存储空间就会不够用 当然&#xff0c;如果你的存…

李彦宏“程序员将不再存在”言论被周鸿祎驳斥,网友怒怼:先把百度程序员都开除了

在 3 月 9 日央视的《对话》开年说节目上&#xff0c;百度创始人、董事长兼 CEO 李彦宏表示&#xff0c;基本上以后不会存在“程序员”这种职业了&#xff0c;因为只要会说话&#xff0c;人人都会具备程序员的能力。 “未来的编程语言只会剩下两种&#xff0c;一种叫做英文&am…

Python和RPA之间的区别和联系

Python是实现RPA的工具之一&#xff0c;且RPA要复杂的多&#xff0c;远不是会Python这么简单。 要理解RPA和Python的区别&#xff0c;先看它们各自做什么。 1、什么时候会用到RPA&#xff1f; 比如你的财务同事需要做财务对账&#xff0c;发票报销&#xff0c;税务申报等&…

【刷题训练】LeetCode125. 验证回文串

验证回文串 题目要求 示例 1&#xff1a; 输入: s “A man, a plan, a canal: Panama” 输出&#xff1a;true 解释&#xff1a;“amanaplanacanalpanama” 是回文串。 示例 2&#xff1a; 输入&#xff1a;s “race a car” 输出&#xff1a;false 解释&#xff1a;“rac…

C#常用数据操作方法详解

文章目录 C#常用数据操作方法详解字符大师&#xff1a;String类的使用艺术字符串截取&#xff1a;Substring示例寻找字符串&#xff1a;IndexOf示例字符串替换&#xff1a;Replace示例字符串分割&#xff1a;Split示例转小写&#xff1a;ToLower示例 数学机械师&#xff1a;Mat…

Java 设计模式系列:行为型-观察者模式

简介 观察者模式是一种行为型设计模式&#xff0c;又被称为发布-订阅&#xff08;Publish/Subscribe&#xff09;模式&#xff0c;它定义了对象之间的一对多依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都会得到通知并被自动更新。 观察者…

全面解析 Axios 请求库的基本使用方法

Axios 是一个流行的基于 Promise 的 HTTP 请求库&#xff0c;用于在浏览器和 Node.js 中进行 HTTP 请求。它提供了简单易用的 API&#xff0c;可以发送各种类型的请求&#xff08;如 GET、POST、PUT、DELETE等&#xff09;&#xff0c;并处理响应数据&#xff0c;Axios 在前端工…

MySQL常见的数据类型

一、数值型 5 种整型 tinyint、smallint、mediumint、int 和 bigint&#xff0c;主要区别就是取值范围不同&#xff0c;还可以在类型前添加一个 限制词 unsigned&#xff0c;不允许添加负数。 3 种浮点型&#xff1a;不能精确存放 float 和 double&#xff0c;可以精确存放 de…

朋友,代码库的“健身方案”要不要了解一下?

你有没有想过&#xff0c;你的代码库可能正面临“健康危机”—— 代码臃肿、低效交付、BUG隐藏、潜藏的安全风险…… “健身达人”上线 如果你的开发、安全和运维团队像是三位“健身达人”&#xff0c;那么极狐GitLab的DevSecOps线上成熟度评估&#xff0c;就是他们的“健身教…

09、用数据变量等控制 vue 项目标签中 CSS 样式的五种方法

通过 vue 中的变量值控制标签/组件样式 Ⅰ、通过 Vue3 的数据变量来控制 h1 标签样式&#xff1a;1、代码为&#xff1a;2、截图为&#xff1a; Ⅱ、通过 vue3 的 computed 来控制 h1 标签样式&#xff1a;1、代码为&#xff1a;2、截图为&#xff1a; Ⅲ、通过 vue3 的 comput…

MySQL gh-ost DDL 变更工具

文章目录 1. MDL 锁介绍2. 变更工具3. gh-ost 原理解析4. 安装部署5. 操作演示5.1. 重点参数介绍5.2. 执行变更5.3. 动态控制 6. 风险提示 1. MDL 锁介绍 MySQL 的锁可以分为四类&#xff1a;MDL 锁、表锁、行锁、GAP 锁&#xff0c;其中除了 MDL 锁是在 Server 层加的之外&am…

SpringBoot vue轮胎批发系统

SpringBoot vue轮胎批发系统 系统功能 首页 图片轮播 轮胎商品 评论 公告信息 搜索 购物车 立即购买 客服中心 登录注册 个人中心 我的订单 我的地址 我的收藏 后台管理 登录 个人中心 用户管理 轮胎分类管理 轮胎商品管理 公告信息管理 客服中心管理 轮播图管理 订单管理 …