探索Python @dataclass的内部原理
之前写过一篇介绍Python
中dataclass
的文章:《掌握python的dataclass,让你的代码更简洁优雅》。
那篇侧重于介绍dataclass
的使用,今天想探索一下这个有趣的特性是如何实现的。
表面上看,dataclass
就是一个普通的装饰器,但是它又在class
上实现了很多神奇的功能,
为我们在Python
中定义和使用class
带来了极大的便利。
如果你也好奇它在幕后是如何工作的,本篇我们就一同揭开Python
中dataclass
的神秘面纱,
深入探究一下其内部原理。
1. dataclass简介
dataclass
为我们提供了一种简洁而高效的方式来定义类,特别是那些主要用于存储数据的类。
它能自动为我们生成一些常用的方法,如__init__
、__repr__
等,大大减少了样板代码的编写。
例如,我在量化中经常用的一个K线数据,用dataclass来定义的话,如下所示:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class KLine:
name: str = "BTC"
open_price: float = 0.0
close_price: float = 0.0
high_price: float = 0.0
low_price: float = 0.0
begin_time: datetime = datetime.now()
if __name__ == "__main__":
kl = KLine()
print(kl)
这样,我们无需手动编写__init__
方法来初始化对象,就可以轻松创建KLine
类的实例,
并且直接打印对象也可以得到清晰,易于阅读的输出。
$ python.exe .\kline.py
KLine(name='BTC', open_price=0.0, close_price=0.0,
high_price=0.0, low_price=0.0,
begin_time=datetime.datetime(2025, 1, 2, 17, 45, 53, 44463))
但这背后究竟发生了什么呢?
2. 核心概念
dataclass
从Python3.7
版本开始,已经加入到标准库中了。
代码就在Python
安装目录中的Lib/dataclasses.py
文件中。
实现这个装饰器功能的核心有两个:__annotations__
属性和exec
函数。
2.1. __annotations__属性
__annotations__
是 Python
中一个隐藏的宝藏,它以字典的形式存储着变量、属性以及函数参数或返回值的类型提示。
对于dataclass
来说,它就像是一张地图,装饰器通过它来找到用户定义的字段。
比如,在上面的KLine
类中,__annotations__
会返回字段的相关信息。
这使得dataclass
装饰器能够清楚地知道类中包含哪些字段以及它们的类型,为后续的操作提供了关键信息。
if __name__ == "__main__":
print(KLine.__annotations__)
# 运行结果:
{'name': <class 'str'>, 'open_price': <class 'float'>,
'close_price': <class 'float'>, 'high_price': <class 'float'>,
'low_price': <class 'float'>, 'begin_time': <class 'datetime.datetime'>}
2.2. exec 函数
exec
函数堪称dataclass
实现的魔法棒,它能够将字符串形式的代码转换为 Python
对象。
在dataclass
的世界里,它被用来创建各种必要的方法。
我们可以通过构建函数定义的字符串,然后使用exec
将其转化为真正的函数,并添加到类中。
这就是dataclass
装饰器能够自动生成__init__
、__repr__
等方法的秘密所在。
下面的代码通过exec
,将一个字符串代码转换成一个真正可使用的函数。
# 定义一个存储代码的字符串
code_string = """
def greet(name):
print(f"Hello, {name}!")
"""
# 使用 exec 函数执行代码字符串
exec(code_string)
# 调用通过 exec 生成的函数
greet("Alice")
3. 自定义dataclass装饰器
掌握了上面的核心概念,我们就可以开始尝试实现自己的dataclass
装饰器。
当然,这里只是简单实现个雏形,目的是为了了解Python
标准库中dataclass
的原理。
下面主要实现两个功能__init__
和__repr__
。
通过这两个功能来理解dataclass
的实现原理。
3.1. 定义架构
我们首先定义一个dataclass
装饰器,它的结构如下:
def dataclass(cls=None, init=True, repr=True):
def wrap(cls):
# 这里将对类进行修改
return cls
if cls is None:
return wrap
return wrap(cls)
接下来,我们在这个装饰器中实现__init__
和__repr__
。
3.2. 初始化:init
当init
参数为True
时,我们为类添加__init__
方法。
通过_init_fn
函数来实现,它会根据类的字段生成__init__
方法的函数定义字符串,然后使用_create_fn
函数将其转换为真正的方法并添加到类中。
def _create_fn(cls, name, fn):
ns = {}
exec(fn, None, ns)
method = ns[name]
setattr(cls, name, method)
def _init_fn(cls, fields):
args = ", ".join(fields)
lines = [f"self.{field} = {field}" for field in fields]
body = "\n".join(f" {line}" for line in lines)
txt = f"def __init__(self, {args}):\n{body}"
_create_fn(cls, "__init__", txt)
3.3. 美化输出:repr
__repr__
方法让我们能够以一种清晰易读的方式打印出类的实例。
为了实现这个功能,我们创建_repr_fn
函数,它生成__repr__
方法的定义字符串。
这个方法会获取实例的__dict__
属性中的所有变量,并使用 f-string
进行格式化输出。
def _repr_fn(cls, fields):
txt = (
"def __repr__(self):\n"
" fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
" return f'{self.__class__.__name__}({\"\\n \".join(fields)})'"
)
_create_fn(cls, "__repr__", txt)
3.4. 合在一起
最终的代码如下,代码中使用的是自己的dataclass
装饰器,而不是标准库中的dataclass
。
from datetime import datetime
def dataclass(cls=None, init=True, repr=True):
def wrap(cls):
fields = cls.__annotations__.keys()
if init:
_init_fn(cls, fields)
if repr:
_repr_fn(cls, fields)
return cls
if cls is None: # 如果装饰器带参数
return wrap
return wrap(cls)
def _create_fn(cls, name, fn):
ns = {}
exec(fn, None, ns)
method = ns[name]
setattr(cls, name, method)
def _init_fn(cls, fields):
args = ", ".join(fields)
lines = [f"self.{field} = {field}" for field in fields]
body = "\n".join(f" {line}" for line in lines)
txt = f"def __init__(self, {args}):\n{body}"
_create_fn(cls, "__init__", txt)
def _repr_fn(cls, fields):
txt = (
"def __repr__(self):\n"
" fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
" return f'{self.__class__.__name__}({\"\\n \".join(fields)})'"
)
_create_fn(cls, "__repr__", txt)
@dataclass
class KLine:
name: str = "BTC"
open_price: float = 0.0
close_price: float = 0.0
high_price: float = 0.0
low_price: float = 0.0
begin_time: datetime = datetime.now()
if __name__ == "__main__":
kl = KLine(
name="ETH",
open_price=1000.5,
close_price=3200.5,
high_price=3400,
low_price=200,
begin_time=datetime.now(),
)
print(kl)
运行的效果如下:
可以看出,我们自己实现的dataclass
装饰器也可以实现类的初始化和美化输出,这里输出时每个属性占一行。
4. 总结
通过自定义dataclass
装饰器的构建过程,我们深入了解了 Python
中dataclass
的内部原理。
利用__annotations__
获取字段信息,借助exec
创建各种方法,从而实现简洁高效的dataclass
定义。
不过,实际的 Python
标准库中的dataclass
还有更多的功能和优化,了解了其原理之后,可以参考它的源码再进一步学习。
探索Python @dataclass的内部原理的更多相关文章
- Git内部原理探索
目录 前言 Git分区 .git版本库里的文件/目录是干什么的 Git是如何存储文件信息的 当我们执行git add.git commit时,Git背后做了什么 Git分支的本质是什么 HEAD引用 ...
- 探索 Python 学习
Python 是一种敏捷的.动态类型化的.极富表现力的开源编程语言,可以被自由地安装到多种平台上(参阅 参考资料).Python 代码是被解释的.如果您对编辑.构建和执行循环较为熟悉,则 Python ...
- Python程序的执行原理(转载)
Python程序的执行原理 2013-09-17 10:35 佚名 tech.uc 1. 过程概述 Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令 ...
- JavaScript内部原理实践——真的懂JavaScript吗?(转)
通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3 JavaScript内部原理的文章, 从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解. 但 ...
- 谈谈 Python 程序的运行原理
因为我的个人网站 restran.net 已经启用,博客园的内容已经不再更新.请访问我的个人网站获取这篇文章的最新内容,谈谈 Python 程序的运行原理 这篇文章准确说是『Python 源码剖析』的 ...
- Git 内部原理--初探 .git
说到Git大家应该都非常熟悉,几乎每天都会用到它.在日常使用过程中,我们貌似并不需要关注其内部的原理,只需要记住那几个常用的命令,就可以说自己是会Git的人了.可是,事实真的是这样子的吗?今天我们就来 ...
- day54_9_18视图层某内部原理(fbv和cbv)与模板层
一.render内部原理. 在render中往往需要返回三个参数,request,模板和一些键值对. 键值对中存储的是需要对模板渲染的值. 如果手动实现可以如下: from django.templa ...
- Python函数装饰器原理与用法详解《摘》
本文实例讲述了Python函数装饰器原理与用法.分享给大家供大家参考,具体如下: 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值 ...
- 深入探索Android热修复技术原理读书笔记 —— 代码热修复技术
在前一篇文章 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍中,对热修复技术进行了介绍,下面将详细介绍其中的代码修复技术. 1 底层热替换原理 在各种 Android 热修复方案中 ...
- Python 中生成器的原理
生成器的使用 在 Python 中,如果一个函数定义的内部使用了 yield 关键字,那么在执行函数的时候返回的是一个生成器,而不是常规函数的返回值. 我们先来看一个常规函数的定义,下面的函数 f() ...
随机推荐
- Vue 中 v-html 无法被 style scoped 渲染的问题
假设有这么一个 vue 组件: <template> <div v-html="docPreview"/> </template> <st ...
- C++ 命令行传参 参数使用 坐标参数的转换
目录 1. 什么是命令行传参 2. 如何传参 3. 应用实例 4. 问题 1. 什么是命令行传参 命令行传参就是在 cmd 命令提示符, 或者 Linux shell 中使用可执行程序时, 可以添加 ...
- 2.6 使用dd命令安装Linux系统
面对大批量服务器的安装,人们往往热衷于选择"无人值守安装"的方式,而此方式需要对服务器进行过多的配置,并不适合初学者. 无人值守安装(Kickstart),又称全自动安装,其工作原 ...
- openEuler搭建k8s(1.28.2版本)
目录 k8s搭建(1.28.2版本) 1. 安装containerd 1.1 下载tar包 1.2 编写服务单元文件 2. 安装runc 3. 安装cni插件 3.1 下载文件 3.2 设置crict ...
- Codeforces Round 988 (Div. 3) E题解析
E题 题目链接 Codeforces Round 988 (Div. 3) 题目描述 题目的思路 根据题目的意思,我们可以推断出算法时间复杂度应该在O(N) 对于这道题而言,我们可以分析下思路 首先我 ...
- PCA主成分分析的Python实现
技术背景 PCA主成分分析在数据处理和降维中经常被使用到,是一个非常经典的降维算法,本文提供一个PCA降维的流程分解,和对应的Python代码实现. 二维数据生成 如果没有自己的测试数据,我们可以生成 ...
- WxPython跨平台开发框架之用户选择和标签组件的设计
在系统的权限管理中,往往都会涉及到用户的选择处理,特别是基于角色的访问控制中,很多情况下需要用到选择用户的处理.本篇随笔,基于WxPython跨平台开发框架,采用原有开发框架成熟的一套权限系统理念,对 ...
- Trino 436 - 使用教程(亲测,详细)
第一章 Trino简介 1. Trino概述 Trino是一个分布式SQL查询引擎,旨在查询分布在一个或多个异构数据源上的大型数据集.如果使用的是数TB或数PB的数据,那么很可能使用的是与Hadoop ...
- Centos7.8安装Gitlab
公司为了合规性考虑,需要自己搭建私有化版的github.那不用想,肯定要上GitLab了. 项目背景: 服务器:华为云ECS,需要上公网,并在安全组打开80端口访问. 用户:关闭公开注册,新建用户后, ...
- axios 取消请求 (2023-10-10更新)
axios 文档 配置局部取消请求 这种相当于局部的取消请求,作用于单个请求中 import axios from 'axios' const source = axios.cancelToken.s ...