探索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() ...
随机推荐
- MISC 高手进阶区 1-5
1.reverseME 题目描述 无 附件 一个 .jpg 的图片 是一个flag字符串的图片镜像. reverse V-T If you reverse the order of a set of ...
- NDT算法(深蓝学院)
- 4.8 Linux解压.bz2格式的文件(bunzip2命令)
要解压".bz2"格式的压缩包文件,除了使用"bzip2 -d 压缩包名"命令外,还可以使用 bunzip2 命令. bunzip2 命令的使用和 gunzip ...
- 鸿蒙Navigation页面生命周期
Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放.其生命周期大致可分为三类,自定义组件生命周期.通用组件生命周期和自有生命周期.其中,abou ...
- Cargo deny安装指路
本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0许可协议.转载请注明来自 唯你 简介 cargo deny 是一个 Rust 工具,用于检查项目依赖项的许可证.安全性和其他合规性问题. ...
- Air780EP之RC522开发板,你了解吗?
本文讲解合宙Air780EP开发板RC522实例. 本文档适用于Air780EP开发板: 关联文档和使用工具: rc522 - rc522 非接触式读写卡驱动 - LuatOS 文档: LuatO ...
- python 3.7环境安装并使用csv
因为调换需要,进了另外一个维护组,需要用python解析excel csv,所以就下载了一下他们需要的python3.7 如何做呢,看步骤 1.去官网 2.找版本 3.下源码 4.解压出来进入文件夹 ...
- 添加linux alias
有时候需要用alias做几个简单好记的命令方便快速输入 可以使用alias进行修改别名 在我的Ubuntu系统下 看一下 /etc/bash.bashrc 文件 在后面加入这个代码 # add the ...
- 啃啃老菜:Spring IOC核心源码学习(一)
啃啃老菜:Spring IOC核心源码学习(一) 本文主要以spring ioc容器基本代码骨架为切入点,理解ioc容器的基本代码组件结构,各代码组件细节剖析将放在后面的学习文章里. 关于IOC容器 ...
- 0-JavaScript入门
1.1 JS用途 JavaScript属于一门脚本语言,可用于前端给网页添加一些动态效果. 也可用于node平台,开发服务器 React.Vue框架底层也是通过JS实现 1.2 JS组成 前端分为三层 ...