使用 dacite.from_dict 动态添加数据类字段

Adding dataclass fields dynamically with dacite.from_dict

我正在使用 英安岩 将 Python 字典转换为数据类。有没有办法动态地向数据类添加字段?如下例所示,数据类“Parameters”仅定义了一个时间序列“timeseriesA”,但可能还有其他无法声明的时间序列(通过字典提供)。

from dataclasses import asdict, dataclass
from typing import Dict, List, Optional

from dacite import from_dict

@dataclass(frozen = True)
class TimeSeries:
  name: str
  unit: str
  data: Optional[List[float]]
  
@dataclass(frozen = True)
class Parameters:
  timeseriesA: TimeSeries
  
@dataclass(frozen = True)
class Data:
  parameters: Parameters
  
  @classmethod
  def fromDict(cls, data: Dict) -> 'Data':
    return from_dict(cls, data)

  @classmethod
  def toDict(cls) -> Dict:
    return asdict(cls)

  
def main() -> None:

  d: Dict = {
    'parameters': {
      'timeseriesA': {
        'name': 'nameA',
        'unit': 'USD',
        'data': [10, 20, 30, 40]
      },
      'timeseriesB': {
        'name': 'nameB',
        'unit': 'EUR',
        'data': [60, 30, 40, 50]
      }
    }
  }

  data: Data = Data.fromDict(d)

if __name__ == '__main__':
  main()

在此示例中,英安岩 将忽略“timeseriesB”,但应添加为“参数”数据类的字段。

一般来说,在定义 class 之后动态添加 字段 到数据 class 不是好的做法。但是,由于源 dict 中字段的 动态 性质,这确实为在数据 class 中使用 dict 提供了一个很好的用例] 对象。

这是一个使用 dict 字段处理源对象中键的 动态 映射的简单示例,使用 dataclass-wizard 是还有一个类似的 JSON 序列化库。下面概述的方法处理 dict 对象中的无关数据,例如 timeseriesB

from __future__ import annotations

from dataclasses import dataclass
from dataclass_wizard import JSONWizard


@dataclass(frozen=True)
class Data(JSONWizard):
    parameters: dict[str, TimeSeries]


@dataclass(frozen=True)
class TimeSeries:
    name: str
    unit: str
    data: list[float] | None


data: dict = {
    'parameters': {
        'timeseriesA': {
            'name': 'nameA',
            'unit': 'USD',
            'data': [10, 20, 30, 40]
        },
        'timeseriesB': {
            'name': 'nameB',
            'unit': 'EUR',
            'data': [60, 30, 40, 50]
        }
    }
}


def main():
    # deserialize from dict
    d = Data.from_dict(data)
    print(d.parameters['timeseriesB'].unit)  # EUR

    print(repr(d)) 
    # Data(parameters={'timeseriesA': TimeSeries(name='nameA', unit='USD', data=[10.0, 20.0, 30.0, 40.0]),
    #                  'timeseriesB': TimeSeries(name='nameB', unit='EUR', data=[60.0, 30.0, 40.0, 50.0])})


if __name__ == '__main__':
    main()

dataclass-wizard 诚然不会像 dacite 那样执行 strict 类型检查,而是执行 implicit在可能的情况下,键入强制转换,例如 str 到带注释的 int。也许因此,它总体上要快得多;另一件好事是序列化甚至比内置 dataclasses.asdict 稍微快一点:-)

这里有一些快速测试:

from dataclasses import asdict, dataclass
from typing import Dict, List, Optional

from dacite import from_dict
from dataclass_wizard import JSONWizard
from timeit import timeit


@dataclass(frozen=True)
class TimeSeries:
    name: str
    unit: str
    data: Optional[List[float]]


@dataclass(frozen=True)
class Parameters:
    timeseriesA: TimeSeries


@dataclass(frozen=True)
class Data:
    parameters: Parameters

    @classmethod
    def fromDict(cls, data: Dict) -> 'Data':
        return from_dict(cls, data)

    def toDict(self) -> Dict:
        return asdict(self)


@dataclass(frozen=True)
class ParametersWizard:
    # renamed because default key transform is `camelCase` -> `snake_case`
    timeseries_a: TimeSeries


@dataclass(frozen=True)
class DataWizard(JSONWizard):
    # enable debug mode in case of incorrect types etc.
    class _(JSONWizard.Meta):
        debug_enabled = True

    parameters: ParametersWizard


data: Dict = {
    'parameters': {
        'timeseriesA': {
            'name': 'nameA',
            'unit': 'USD',
            'data': [10, 20, 30, 40]
        },
        'timeseriesB': {
            'name': 'nameB',
            'unit': 'EUR',
            'data': [60, 30, 40, 50]
        }
    }
}


def main():
    n = 10_000

    print(f"From Dict:        {timeit('Data.fromDict(data)', globals=globals(), number=n):.3f}")
    print(f"From Dict (Wiz):  {timeit('DataWizard.from_dict(data)', globals=globals(), number=n):.3f}")

    data_1: Data = Data.fromDict(data)
    data_wiz: Data = DataWizard.from_dict(data)

    g = globals().copy()
    g.update(locals())

    print(f"To Dict:        {timeit('data_1.toDict()', globals=g, number=n):.3f}")
    print(f"To Dict (Wiz):  {timeit('data_wiz.to_dict()', globals=g, number=n):.3f}")


if __name__ == '__main__':
    main()

结果,在我的电脑上 (Windows):

From Dict:        1.663
From Dict (Wiz):  0.059
To Dict:        0.105
To Dict (Wiz):  0.057