深入理解Python @dataclass的内部原理
作者:wang_yb
之前写过一篇介绍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的内部原理的文章就介绍到这了,更多相关Python @dataclass原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!