探索Python @dataclass的内部原理

打印 上一主题 下一主题

主题 840|帖子 840|积分 2520

之前写过一篇介绍Python中dataclass的文章:《掌握python的dataclass,让你的代码更简便优雅》
那篇偏重于介绍dataclass的使用,今天想探索一下这个有趣的特性是怎样实现的。
表面上看,dataclass就是一个普通的装饰器,但是它又在class上实现了很多神奇的功能,
为我们在Python中界说和使用class带来了极大的便利。
假如你也好奇它在幕后是怎样工作的,本篇我们就一同揭开Python中dataclass的秘密面纱,
深入探究一下其内部原理。
1. dataclass简介

dataclass为我们提供了一种简便而高效的方式来界说类,特别是那些主要用于存储数据的类。
它能自动为我们生成一些常用的方法,如__init__、__repr__等,大大镌汰了样板代码的编写。
例如,我在量化中经常用的一个K线数据,用dataclass来界说的话,如下所示:
  1. from dataclasses import dataclass
  2. from datetime import datetime
  3. @dataclass
  4. class KLine:
  5.     name: str = "BTC"
  6.     open_price: float = 0.0
  7.     close_price: float = 0.0
  8.     high_price: float = 0.0
  9.     low_price: float = 0.0
  10.     begin_time: datetime = datetime.now()
  11. if __name__ == "__main__":
  12.     kl = KLine()
  13.     print(kl)
复制代码
这样,我们无需手动编写__init__方法来初始化对象,就可以轻松创建KLine类的实例,
并且直接打印对象也可以得到清晰,易于阅读的输出。
  1. $  python.exe .\kline.py
  2. KLine(name='BTC', open_price=0.0, close_price=0.0,
  3. high_price=0.0, low_price=0.0,
  4. 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装饰器能够清晰地知道类中包含哪些字段以及它们的类型,为后续的操作提供了关键信息。
  1. if __name__ == "__main__":
  2.     print(KLine.__annotations__)
  3. # 运行结果:
  4. {'name': <class 'str'>, 'open_price': <class 'float'>,
  5. 'close_price': <class 'float'>, 'high_price': <class 'float'>,
  6. 'low_price': <class 'float'>, 'begin_time': <class 'datetime.datetime'>}
复制代码
2.2. exec 函数

exec函数堪称dataclass实现的邪术棒,它能够将字符串形式的代码转换为 Python 对象。
在dataclass的天下里,它被用来创建各种必要的方法。
我们可以通过构建函数界说的字符串,然后使用exec将其转化为真正的函数,并添加到类中。
这就是dataclass装饰器能够自动生成__init__、__repr__等方法的秘密所在。
下面的代码通过exec,将一个字符串代码转换成一个真正可使用的函数。
  1. # 定义一个存储代码的字符串
  2. code_string = """
  3. def greet(name):
  4.     print(f"Hello, {name}!")
  5. """
  6. # 使用 exec 函数执行代码字符串
  7. exec(code_string)
  8. # 调用通过 exec 生成的函数
  9. greet("Alice")
复制代码
3. 自界说dataclass装饰器

掌握了上面的焦点概念,我们就可以开始实验实现自己的dataclass装饰器。
当然,这里只是简单实现个雏形,目的是为了相识Python尺度库中dataclass的原理。
下面主要实现两个功能__init__和__repr__。
通过这两个功能来理解dataclass的实现原理。
3.1. 界说架构

我们起首界说一个dataclass装饰器,它的结构如下:
  1. def dataclass(cls=None, init=True, repr=True):
  2.     def wrap(cls):
  3.         # 这里将对类进行修改
  4.         return cls
  5.     if cls is None:
  6.         return wrap
  7.     return wrap(cls)
复制代码
接下来,我们在这个装饰器中实现__init__和__repr__。
3.2. 初始化:init

当init参数为True时,我们为类添加__init__方法。
通过_init_fn函数来实现,它会根据类的字段生成__init__方法的函数界说字符串,然后使用_create_fn函数将其转换为真正的方法并添加到类中。
  1. def _create_fn(cls, name, fn):
  2.     ns = {}
  3.     exec(fn, None, ns)
  4.     method = ns[name]
  5.     setattr(cls, name, method)
  6. def _init_fn(cls, fields):
  7.     args = ", ".join(fields)
  8.     lines = [f"self.{field} = {field}" for field in fields]
  9.     body = "\n".join(f"  {line}" for line in lines)
  10.     txt = f"def __init__(self, {args}):\n{body}"
  11.     _create_fn(cls, "__init__", txt)
复制代码
3.3. 美化输出:repr

__repr__方法让我们能够以一种清晰易读的方式打印出类的实例。
为了实现这个功能,我们创建_repr_fn函数,它生成__repr__方法的界说字符串。
这个方法会获取实例的__dict__属性中的全部变量,并使用 f-string 进行格式化输出。
  1. def _repr_fn(cls, fields):
  2.     txt = (
  3.         "def __repr__(self):\n"
  4.         "    fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
  5.         "    return f'{self.__class__.__name__}({"\\n ".join(fields)})'"
  6.     )
  7.     _create_fn(cls, "__repr__", txt)
复制代码
3.4. 合在一起

终极的代码如下,代码中使用的是自己的dataclass装饰器,而不是尺度库中的dataclass。
  1. from datetime import datetimedef 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):
  2.     ns = {}
  3.     exec(fn, None, ns)
  4.     method = ns[name]
  5.     setattr(cls, name, method)
  6. def _init_fn(cls, fields):
  7.     args = ", ".join(fields)
  8.     lines = [f"self.{field} = {field}" for field in fields]
  9.     body = "\n".join(f"  {line}" for line in lines)
  10.     txt = f"def __init__(self, {args}):\n{body}"
  11.     _create_fn(cls, "__init__", txt)def _repr_fn(cls, fields):
  12.     txt = (
  13.         "def __repr__(self):\n"
  14.         "    fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
  15.         "    return f'{self.__class__.__name__}({"\\n ".join(fields)})'"
  16.     )
  17.     _create_fn(cls, "__repr__", txt)@dataclassclass 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还有更多的功能和优化,相识了其原理之后,可以参考它的源码再进一步学习。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表