之前写过一篇介绍Pythondataclass的文章:《掌握python的dataclass,让你的代码更简洁优雅》

那篇侧重于介绍dataclass的使用,今天想探索一下这个有趣的特性是如何实现的。

表面上看,dataclass就是一个普通的装饰器,但是它又在class上实现了很多神奇的功能,

为我们在Python中定义和使用class带来了极大的便利。

如果你也好奇它在幕后是如何工作的,本篇我们就一同揭开Pythondataclass的神秘面纱,

深入探究一下其内部原理。

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. 核心概念

dataclassPython3.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装饰器的构建过程,我们深入了解了 Pythondataclass的内部原理。

利用__annotations__获取字段信息,借助exec创建各种方法,从而实现简洁高效的dataclass定义。

不过,实际的 Python标准库中的dataclass还有更多的功能和优化,了解了其原理之后,可以参考它的源码再进一步学习。

探索Python @dataclass的内部原理的更多相关文章

  1. Git内部原理探索

    目录 前言 Git分区 .git版本库里的文件/目录是干什么的 Git是如何存储文件信息的 当我们执行git add.git commit时,Git背后做了什么 Git分支的本质是什么 HEAD引用 ...

  2. 探索 Python 学习

    Python 是一种敏捷的.动态类型化的.极富表现力的开源编程语言,可以被自由地安装到多种平台上(参阅 参考资料).Python 代码是被解释的.如果您对编辑.构建和执行循环较为熟悉,则 Python ...

  3. Python程序的执行原理(转载)

    Python程序的执行原理 2013-09-17 10:35 佚名 tech.uc  1. 过程概述 Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令 ...

  4. JavaScript内部原理实践——真的懂JavaScript吗?(转)

    通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3 JavaScript内部原理的文章, 从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解. 但 ...

  5. 谈谈 Python 程序的运行原理

    因为我的个人网站 restran.net 已经启用,博客园的内容已经不再更新.请访问我的个人网站获取这篇文章的最新内容,谈谈 Python 程序的运行原理 这篇文章准确说是『Python 源码剖析』的 ...

  6. Git 内部原理--初探 .git

    说到Git大家应该都非常熟悉,几乎每天都会用到它.在日常使用过程中,我们貌似并不需要关注其内部的原理,只需要记住那几个常用的命令,就可以说自己是会Git的人了.可是,事实真的是这样子的吗?今天我们就来 ...

  7. day54_9_18视图层某内部原理(fbv和cbv)与模板层

    一.render内部原理. 在render中往往需要返回三个参数,request,模板和一些键值对. 键值对中存储的是需要对模板渲染的值. 如果手动实现可以如下: from django.templa ...

  8. Python函数装饰器原理与用法详解《摘》

    本文实例讲述了Python函数装饰器原理与用法.分享给大家供大家参考,具体如下: 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值 ...

  9. 深入探索Android热修复技术原理读书笔记 —— 代码热修复技术

    在前一篇文章 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍中,对热修复技术进行了介绍,下面将详细介绍其中的代码修复技术. 1 底层热替换原理 在各种 Android 热修复方案中 ...

  10. Python 中生成器的原理

    生成器的使用 在 Python 中,如果一个函数定义的内部使用了 yield 关键字,那么在执行函数的时候返回的是一个生成器,而不是常规函数的返回值. 我们先来看一个常规函数的定义,下面的函数 f() ...

随机推荐

  1. Vue 中 v-html 无法被 style scoped 渲染的问题

    假设有这么一个 vue 组件: <template> <div v-html="docPreview"/> </template> <st ...

  2. C++ 命令行传参 参数使用 坐标参数的转换

    目录 1. 什么是命令行传参 2. 如何传参 3. 应用实例 4. 问题 1. 什么是命令行传参 命令行传参就是在 cmd 命令提示符, 或者 Linux shell 中使用可执行程序时, 可以添加 ...

  3. 2.6 使用dd命令安装Linux系统

    面对大批量服务器的安装,人们往往热衷于选择"无人值守安装"的方式,而此方式需要对服务器进行过多的配置,并不适合初学者. 无人值守安装(Kickstart),又称全自动安装,其工作原 ...

  4. 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 ...

  5. Codeforces Round 988 (Div. 3) E题解析

    E题 题目链接 Codeforces Round 988 (Div. 3) 题目描述 题目的思路 根据题目的意思,我们可以推断出算法时间复杂度应该在O(N) 对于这道题而言,我们可以分析下思路 首先我 ...

  6. PCA主成分分析的Python实现

    技术背景 PCA主成分分析在数据处理和降维中经常被使用到,是一个非常经典的降维算法,本文提供一个PCA降维的流程分解,和对应的Python代码实现. 二维数据生成 如果没有自己的测试数据,我们可以生成 ...

  7. WxPython跨平台开发框架之用户选择和标签组件的设计

    在系统的权限管理中,往往都会涉及到用户的选择处理,特别是基于角色的访问控制中,很多情况下需要用到选择用户的处理.本篇随笔,基于WxPython跨平台开发框架,采用原有开发框架成熟的一套权限系统理念,对 ...

  8. Trino 436 - 使用教程(亲测,详细)

    第一章 Trino简介 1. Trino概述 Trino是一个分布式SQL查询引擎,旨在查询分布在一个或多个异构数据源上的大型数据集.如果使用的是数TB或数PB的数据,那么很可能使用的是与Hadoop ...

  9. Centos7.8安装Gitlab

    公司为了合规性考虑,需要自己搭建私有化版的github.那不用想,肯定要上GitLab了. 项目背景: 服务器:华为云ECS,需要上公网,并在安全组打开80端口访问. 用户:关闭公开注册,新建用户后, ...

  10. axios 取消请求 (2023-10-10更新)

    axios 文档 配置局部取消请求 这种相当于局部的取消请求,作用于单个请求中 import axios from 'axios' const source = axios.cancelToken.s ...