【问题标题】:Create a dataclass with field names automatically generated from dict keys使用从字典键自动生成的字段名称创建一个数据类
【发布时间】:2022-06-26 09:17:39
【问题描述】:

我想从dict 创建一个dataclass,不仅具有dict 的值,而且还具有自动识别为dataclass 的字段名称的keys。

输入是

d = {'a': 3, 'b': 7}

现在我想做这样的事情

import dataclasses

# Hocus pocus
X = dataclasses.dataclass_from_dict(name='X', the_dict=d)

print(X)  # <class '__main__.X'> 

z = X(a=3, b=99)                                                                                                         
print(z)  # X(a=3, b=99)

这里的重点是,数据类及其字段是根据字典的键自动创建的。所以不需要知道dict的结构和key。

到目前为止我尝试了什么

我尝试了dataclasses.make_dataclass(),但结果 (AUTO) 与以通常方式创建的数据类 (MANUAL) 不同。

>>> d = {'a': 3, 'b': 7}
>>> AUTO = dataclasses.make_dataclass('AUTO', [(key, type(d[key])) for key in d])
>>> @dataclass
... class MANUAL:
...   a: int
...   b: int
...
>>> AUTO
<class 'types.AUTO'>
>>> MANUAL
<class '__main__.MANUAL'>  

【问题讨论】:

  • 为什么?为什么不直接使用字典?
  • 动态数据类似乎类似于变量变量——不是很有用。
  • 使用字典我必须输入d["a"],但我想做d.a。但我不能使用namedtuples,因为我需要操作字段。
  • 但是如果字段名称是从字典中动态确定的,则不能使用d.a。您的代码如何知道 a 是有效的字段名称?
  • 这是正确的,您也会失去来自 IDE 的类型提示,这可以说是数据类的优势之一。例如,如果您对动态生成的数据类执行 d.a,您的 IDE 不知道它是 str 还是 int 类型。听起来你想要的是一个 dotdict - dict 对象的点访问。

标签: python python-3.x


【解决方案1】:

在这种情况下,类型提示和自动完成的好处将在很大程度上被忽略,因此我个人建议使用定制的DotDict 方法,如下所述。

我很好奇,所以我用dataclasses.make_dataclass 方法来计时。如果您有兴趣,我还附上了我用于基准测试的完整测试代码。

更新(6/22):我为此创建了一个库并发布在 pypi - dotwiz。一探究竟。它应该和下面的方法一样快,但有一些明显的改进。

import dataclasses
from timeit import timeit


class DotDict(dict):

    __getattr__ = dict.__getitem__
    __delattr__ = dict.__delitem__

    def __repr__(self):
        fields = [f'{k}={v!r}' for k, v in self.items()]
        return f'{self.__class__.__name__}({", ".join(fields)})'


def make_dot_dict(input_dict: dict) -> DotDict:
    """
    Helper method to generate and return a `DotDict` (dot-access dict) from a
    Python `dict` object.

    """
    return DotDict(
        (
            k,
            make_dot_dict(v) if isinstance(v, dict)
            else [make_dot_dict(e) if isinstance(e, dict) else e
                  for e in v] if isinstance(v, list)
            else v
        ) for k, v in input_dict.items()
    )


def main():
    d = {'a': 3, 'b': 1, 'c': {'aa': 33, 'bb': [{'x': 77}]}}
    X = dataclasses.make_dataclass('X', d)

    n = 10_000
    globals().update(locals())

    time_to_make_dataclass = timeit("dataclasses.make_dataclass('X', d)", number=n, globals=globals())
    time_to_instantiate_dataclass = timeit("X(**d)", number=n, globals=globals())
    time_to_instantiate_dot_dict = timeit("make_dot_dict(d)", number=n, globals=globals())

    print(f'dataclasses.make_dataclass:     {time_to_make_dataclass:.3f}')
    print(f'instantiate dataclass (X):      {time_to_instantiate_dataclass:.3f}')
    print(f'instantiate dotdict (DotDict):  {time_to_instantiate_dot_dict:.3f}')

    print()

    create_instance_perc = time_to_instantiate_dot_dict / time_to_instantiate_dataclass
    total_time_perc = (time_to_make_dataclass + time_to_instantiate_dataclass) / time_to_instantiate_dot_dict

    print(f'It is {create_instance_perc:.0f}x faster to create a dataclass instance')
    print(f'It is {total_time_perc:.0f}x faster (overall) to create a DotDict instance')

    # create new `DotDict` and check we can use dot-access as well as dict-access

    dd = make_dot_dict(d)

    assert dd.b == 1
    assert dd.c.aa == 33
    assert dd['c']['aa'] == 33
    assert dd.c.bb[0].x == 77

    # create new dataclass `X` instance
    x = X(**d)

    # assert result is same between both DotDict and dataclass approach
    assert dd == x.__dict__


if __name__ == '__main__':
    main()

我在我的 Mac(M1 芯片)上收到以下结果:

dataclasses.make_dataclass:     1.342
instantiate dataclass (X):      0.002
instantiate dotdict (DotDict):  0.013

It is 6x faster to create a dataclass instance
It is 100x faster (overall) to create a DotDict instance

正如预期的那样,我发现DotDict 方法在一般情况下的整体性能要好得多。这主要是因为它不需要动态生成一个新的类,扫描一次dict对象就可以生成dataclass字段及其类型。

虽然最初创建了类,但我惊讶地发现数据类方法在平均情况下的性能大约提高了 5 倍。

【讨论】:

    【解决方案2】:

    将dataclasses.make_dataclass 与带有适当键的字典一起使用。

    X = dataclasses.make_dataclass('X', d)
    

    然后你可以使用同种dict来实例化X。

    z = X(**d)
    

    【讨论】:

    • 请先检查您的代码并告诉我们您的确切 Python 版本。在 Python 3.9.10 中,make_dataclass() 不只接受一个参数。
    • 糟糕,我在测试时发现了这一点,但忘了实际修复答案。
    • 但是为什么我在我的问题的“到目前为止我尝试了什么”部分中描述的差异?这个差异有关系吗?
    • 我不知道你为什么关心repr。
    • 表示这里有问题。顺便说一句,酸洗是不可能的。
    猜你喜欢
    • 1970-01-01
    • 2022-11-15
    • 2011-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-27
    • 2021-02-19
    • 2021-07-07
    相关资源
    最近更新 更多