如何将Python数据类转换为字符串文字字典?

问题描述 投票:0回答:4

给定如下数据类:

class MessageHeader(BaseModel):
    message_id: uuid.UUID

    def dict(self, **kwargs):
        return json.loads(self.json())

当我在

dict
上拨打
MessageHeader
时,我想获取字符串文字的字典 字典的期望结果如下:

{'message_id': '383b0bfc-743e-4738-8361-27e6a0753b5a'}

我想避免使用像

pydantic
这样的第3方库&我不想使用
json.loads(self.json())
,因为有额外的往返

有没有更好的方法将数据类转换为带有上面字符串文字的字典?

python json dictionary python-dataclasses
4个回答
89
投票

您可以使用

dataclasses.asdict

from dataclasses import dataclass, asdict

class MessageHeader(BaseModel):
    message_id: uuid.UUID

    def dict(self):
        return {k: str(v) for k, v in asdict(self).items()}

如果您确定您的类只有字符串值,您可以完全跳过字典理解:

class MessageHeader(BaseModel):
    message_id: uuid.UUID

    dict = asdict

使用此解决方案时要小心,因为如果您在

dataclass
之后扩展
__init__
对象 -
asdict
将忽略任何新添加的内容!使用
vars(x)
x.__dict__
让它始终返回最新的字典。


16
投票

为了绝对的纯粹、不掺假的速度无限的效率,甚至可以让像Chuck Norris这样的人停下来,无助地敬畏地看着,我谦虚地推荐这个计划得非常好- out 方法与

__dict__
:

def dict(self):
    _dict = self.__dict__.copy()
    _dict['message_id'] = str(_dict['message_id'])
    return _dict

对于定义

__slots__
属性 的类(例如
@dataclass(slots=True)
),上述方法很可能不起作用,因为
__dict__
属性在类实例上不可用。在这种情况下,如下所示的高效“登月”方法可能是可行的:

def dict(self):
    body_lines = ','.join(f"'{f}':" + (f'str(self.{f})' if f == 'message_id'
                                       else f'self.{f}') for f in self.__slots__)
    # Compute the text of the entire function.
    txt = f'def dict(self):\n return {{{body_lines}}}'
    ns = {}
    exec(txt, locals(), ns)
    _dict_fn = self.__class__.dict = ns['dict']
    return _dict_fn(self)

万一有人现在在座位边缘摇摇欲坠(我知道,这真的令人难以置信,突破级别的东西)-我已经通过下面的

timeit
模块添加了我的个人计时,希望如此在性能方面更加清晰。

仅供参考,纯

__dict__
的方法不可避免地比dataclasses.asdict()
很多

注意:尽管

__dict__
在这种特殊情况下效果更好,但
dataclasses.asdict()
可能更适合复合字典,例如具有嵌套数据类的字典,或具有可变类型(例如
dict
list
)的值。

from dataclasses import dataclass, asdict, field
from uuid import UUID, uuid4


class DictMixin:
    """Mixin class to add a `dict()` method on classes that define a __slots__ attribute"""

    def dict(self):
        body_lines = ','.join(f"'{f}':" + (f'str(self.{f})' if f == 'message_id'
                                           else f'self.{f}') for f in self.__slots__)
        # Compute the text of the entire function.
        txt = f'def dict(self):\n return {{{body_lines}}}'
        ns = {}
        exec(txt, locals(), ns)
        _dict_fn = self.__class__.dict = ns['dict']
        return _dict_fn(self)


@dataclass
class MessageHeader:
    message_id: UUID = field(default_factory=uuid4)
    string: str = 'a string'
    integer: int = 1000
    floating: float = 1.0

    def dict1(self):
        _dict = self.__dict__.copy()
        _dict['message_id'] = str(_dict['message_id'])
        return _dict

    def dict2(self):
        return {k: str(v) if k == 'message_id' else v
                for k, v in self.__dict__.items()}

    def dict3(self):
        return {k: str(v) if k == 'message_id' else v
                for k, v in asdict(self).items()}


@dataclass(slots=True)
class MessageHeaderWithSlots(DictMixin):
    message_id: UUID = field(default_factory=uuid4)
    string: str = 'a string'
    integer: int = 1000
    floating: float = 1.0

    def dict2(self):
        return {k: str(v) if k == 'message_id' else v
                for k, v in asdict(self).items()}


if __name__ == '__main__':
    from timeit import timeit

    header = MessageHeader()
    header_with_slots = MessageHeaderWithSlots()

    n = 10000
    print('dict1():  ', timeit('header.dict1()', number=n, globals=globals()))
    print('dict2():  ', timeit('header.dict2()', number=n, globals=globals()))
    print('dict3():  ', timeit('header.dict3()', number=n, globals=globals()))

    print('slots -> dict():  ', timeit('header_with_slots.dict()', number=n, globals=globals()))
    print('slots -> dict2(): ', timeit('header_with_slots.dict2()', number=n, globals=globals()))

    print()

    dict__ = header.dict1()
    print(dict__)

    asdict__ = header.dict3()
    print(asdict__)

    assert isinstance(dict__['message_id'], str)
    assert isinstance(dict__['integer'], int)

    assert header.dict1() == header.dict2() == header.dict3()
    assert header_with_slots.dict() == header_with_slots.dict2()

我的 Mac M1 笔记本电脑上的结果:

dict1():   0.005992999998852611
dict2():   0.00800508284009993
dict3():   0.07069579092785716
slots -> dict():   0.00583599996753037
slots -> dict2():  0.07395245810039341

{'message_id': 'b4e17ef9-1a58-4007-9cef-39158b094da2', 'string': 'a string', 'integer': 1000, 'floating': 1.0}
{'message_id': 'b4e17ef9-1a58-4007-9cef-39158b094da2', 'string': 'a string', 'integer': 1000, 'floating': 1.0}

注意: 对于

DictMixin
(命名为
SerializableMixin
)更“完整”的实现,请查看相关答案我也添加了。


7
投票

这是“dataclass to dict”的最佳谷歌结果,上面的答案过于复杂。您可能正在寻找这个:

from dataclasses import dataclass
@dataclass
class MessageHeader():
    uuid: str = "abcd"
vars(MessageHeader()) # or MessageHeader().__dict__

1
投票

受到 @rv.kvetch 的回答的启发,我编写了这个装饰器,它将根据类定义动态生成

asdict
方法的代码。它还支持子类化,这意味着子类将继承超类的属性。

装饰者:

import typing


def generate_dict_method(
        __source: typing.Literal["slots", "annotations"],
        __name: str,
        /,
        **custom_mappings: typing.Callable[[typing.Any], typing.Any]
):
    if custom_mappings is None:
        custom_mappings = dict()

    def decorator(cls):
        attributes = set()
        for mc in cls.__mro__:
            if __source == 'annotations':
                attrs = getattr(mc, "__annotations__", None)
                if attrs:
                    attrs = attrs.keys()
            elif __source == "slots":
                attrs = getattr(mc, "__slots__", None)
            else:
                raise NotImplementedError(__source)
            if attrs:
                attributes.update(attrs)

        if not attributes:
            raise RuntimeError(
                f"Unable to generate `{__name}` method for `{cls.__qualname__}` class: "
                "no attributes found."
            )

        funclocals = {}
        mapping_to_funcname = {}

        for attrname, f in custom_mappings.items():
            funcname = f'__parse_{attrname}'
            funclocals[funcname] = f
            mapping_to_funcname[attrname] = funcname

        body_lines = ','.join([
            f'"{attrname}": ' + (f'self.{attrname}' if attrname not in custom_mappings
                                 else f'{mapping_to_funcname[attrname]}(self.{attrname})')
            for attrname in attributes
        ])
        txt = f'def {__name}(self):\n return {{{body_lines}}}'
        d = dict()
        exec(txt, funclocals, d)
        setattr(cls, __name, d[__name])
        return cls

    return decorator

用途:


from dataclasses import dataclass
import json


@dataclass(slots=True, kw_only=True)
class TestBase:
    i1: int
    i2: int


@generate_dict_method("annotations", "asdict", d=(lambda x: "FUNNY" + json.dumps(x) + "JSON"))
@dataclass(slots=True, kw_only=True)
class Test(TestBase):
    i: int
    b: bool
    s: str
    d: dict


a = Test(i=1, b=True, s="test", d={"test": "test"}, i1=2, i2=3)
print(a.asdict())

输出:

{'d': 'FUNNY{"test": "test"}JSON', 'i': 1, 'i1': 2, 'b': True, 's': 'test', 'i2': 3}

如您所见,您只需为

**custom_mappings
参数和属性名称提供自定义解析器。这样您就可以以任何您认为合适的方式改变属性。

在您的情况下,您可以为

str
属性提供
message_id
函数。

© www.soinside.com 2019 - 2024. All rights reserved.