使用从字典键自动生成的字段名称创建一个数据类

Create a dataclass with field names automatically generated from dict keys

我想从 dict 创建一个 dataclass,不仅要使用 dict 的值,还要使用自动识别为字段名称的 keys dataclass.

输入是

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的结构和关键字。

到目前为止我尝试了什么

我尝试了 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'>  

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

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

然后您可以使用相同类型的 dict.

实例化 X
z = X(**d)

在这种情况下,类型提示和 auto-complete 的好处将在很大程度上被忽略,因此我个人建议采用 custom-built DotDict 方法,如下所述。

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

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 方法在一般情况下 整体表现更好。这主要是因为它不需要动态生成一个新的class,并扫描一次dict对象来生成数据class字段及其类型。

尽管最初创建 class 后,我惊讶地发现 dataclass 方法在一般情况下的性能大约 5x 更好。