动态将字段添加到数据类对象

rom*_*inf 4 python field dynamic python-3.x python-dataclasses

我正在编写一个访问REST API的库。它返回带有用户对象的json。我将其转换为dict,然后将其转换为dataclass对象。问题在于,并非所有字段都是固定的。我想动态添加其他字段(在我的数据类中未指定)。我可以简单地为我的对象分配值,但是它们不会出现在对象表示中,并且dataclasses.asdict函数也不会将它们添加到结果字典中:

from dataclasses import asdict, dataclass

@dataclass
class X:
    i: int

x = X(i=42)
x.s = 'text'

x
# X(i=42)

x.s
# 'text'

asdict(x)
# {'i': 42}
Run Code Online (Sandbox Code Playgroud)

rv.*_*tch 11

更新(6/22):现在已经是 2022 年中期了,我想我应该用我一直在尝试的全新方法来刷新我的答案。我很高兴地宣布我最近发布了一个快速、现代的库,名为dotwiz.

\n

该库可以使用pipdotwiz安装:

\n
pip install dotwiz\n
Run Code Online (Sandbox Code Playgroud)\n

这是我创建的一个小型帮助程序库,它使dict对象可以安全地通过点表示法访问 - 例如而a.b.c不是a[\'b\'][\'c\']. 从个人测试和基准来看,它实际上比类似的东西要快得多-make_dataclass下面有更多信息。

\n

此外,还可以从DotWiz或进行子类化DotWizPlus,这样就可以从 IDE(例如 PyCharm)进行类型提示和自动完成提示。下面是一个简单的例子:

\n
pip install dotwiz\n
Run Code Online (Sandbox Code Playgroud)\n

如果您仍然喜欢使用数据类来对数据进行建模,我在下面包含了我的原始答案,该答案与过去几年相比基本没有变化。

\n

基准测试

\n

以下结果是在配备 M1 芯片、Python 3.10.4 和n=5000迭代的 Mac Pro 上测得的。

\n

创建或实例化对象:

\n
$ python -m timeit -n 5000 -s "from dotwiz import DotWiz" -c "DotWiz(i=42, s=\'text\')"\n5000 loops, best of 5: 425 nsec per loop\n\n$ python -m timeit -n 5000 -s "from dataclasses import make_dataclass" -c "X = make_dataclass(\'X\', [(\'i\', int), (\'s\', str)]); X(i=42, s=\'text\')"\n5000 loops, best of 5: 97.8 usec per loop\n
Run Code Online (Sandbox Code Playgroud)\n

这些时间可能被夸大了,但在这种特殊情况下,它DotWiz看起来make_dataclass. 实际上,我想说平均速度大约快 100 倍。

\n

通过点表示法访问键:

\n
$ python -m timeit -n 5000 -s "from dotwiz import DotWiz" -s "dw = DotWiz(i=42, s=\'text\')" -c "dw.s.lower()"         \n5000 loops, best of 5: 39.7 nsec per loop\n\n$ python -m timeit -n 5000 -s "from dataclasses import make_dataclass" -s "X = make_dataclass(\'X\', [(\'i\', int), (\'s\', str)])" -s "x = X(i=42, s=\'text\')" -c "x.s.lower()"\n5000 loops, best of 5: 39.9 nsec per loop\n
Run Code Online (Sandbox Code Playgroud)\n

访问属性或键的时间看起来基本相同。

\n

将对象序列化为 JSON:

\n
$ python -m timeit -n 5000 -s "import json" -s "from dotwiz import DotWiz" -s "dw = DotWiz(i=42, s=\'text\')" -c "json.dumps(dw)"\n5000 loops, best of 5: 1.1 usec per loop\n\n$ python -m timeit -n 5000 -s "import json" -s "from dotwiz import DotWiz" -s "dw = DotWiz(i=42, s=\'text\')" -c "json.dumps(dw.to_dict())"\n5000 loops, best of 5: 1.46 usec per loop\n\n$ python -m timeit -n 5000 -s "import json" -s "from dataclasses import asdict, make_dataclass" -s "X = make_dataclass(\'X\', [(\'i\', int), (\'s\', str)])" -s "x = X(i=42, s=\'text\')" -c "json.dumps(asdict(x))"\n5000 loops, best of 5: 2.87 usec per loop\n
Run Code Online (Sandbox Code Playgroud)\n

因此,与实例相比,序列化对象实际上看起来要 快 2.5 倍DotWizdataclass

\n

原答案

\n

如前所述,标记为可选的字段应该可以解决该问题。如果没有,请考虑使用 中的属性dataclasses。是的,常规属性应该工作得足够好 - 尽管您必须在 中声明字段__post_init__,这有点不方便。

\n

如果您想为属性设置默认值,以便在创建对象后立即访问 getter 效果很好,并且如果您还希望能够通过构造函数设置默认值,则可以使用称为字段属性的概念;一些库,例如dataclass-wizard提供了对此的全面支持。

\n

用法示例:

\n
from dataclasses import asdict, make_dataclass\n\nfrom dotwiz import DotWiz\n\n\nclass MyTypedWiz(DotWiz):\n    # add attribute names and annotations for better type hinting!\n    i: int\n    s: str\n\n\ndw = MyTypedWiz(i=42, s=\'text\')\nprint(dw)\n# \xe2\x9c\xab(i=42, s=\'text\')\n\nprint(dw.to_dict())\n# {\'i\': 42, \'s\': \'text\'}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

免责声明:我是这个库的创建者(和维护者)。

\n

  • 添加对(声称的)令人印象深刻的基准测试的引用(将性能与 make_dataclass、Box、DotMap 等进行比较):https://github.com/rnag/dotwiz#benchmarks。(我写这篇评论的部分动机是:我希望在网络搜索中找到“dotwiz & dotmap”。) (2认同)

w-m*_*w-m 6

您可以用来即时make_dataclass创建X

X = make_dataclass('X', [('i', int), ('s', str)])
x = X(i=42, s='text')

asdict(x)
# {'i': 42, 's': 'text'}
Run Code Online (Sandbox Code Playgroud)

或作为派生类:

@dataclass
class X:
    i: int

x = X(i=42)
x.__class__ = make_dataclass('Y', fields=[('s', str)], bases=(X,))
x.s = 'text'

asdict(x)
# {'i': 42, 's': 'text'}
Run Code Online (Sandbox Code Playgroud)