【问题标题】:How to override names of dataclasses attributes in Python?如何在 Python 中覆盖数据类属性的名称?
【发布时间】:2022-01-19 13:03:35
【问题描述】:

我正在使用dataclass 解析(HTTP 请求/响应)JSON 对象,今天我遇到了一个问题,需要在我的类中使用 transformation/alias 属性名称。

from dataclasses import dataclass, asdict
from typing import List
import json


@dataclass
class Foo:
    foo_name: str # foo_name -> FOO NAME


@dataclass
class Bar:
    bar_name: str # bar_name -> barName


@dataclass
class Baz:
    baz_name: str # baz_name -> B A Z
    baz_foo: List[Foo] # baz_foo -> BAZ FOO
    baz_bar: List[Bar] # baz_bar -> BAZ BAR

目前:

# encode
baz_e = Baz("name", [{"foo_name": "one"}, {"foo_name": "two"}], [{"bar_name": "first"}])
json_baz_e = json.dumps(asdict(baz_e))
print(json_baz_e)
# {"baz_name": "name", "baz_foo": [{"foo_name": "one"}, {"foo_name": "two"}], "baz_bar": [{"bar_name": "first"}]}


# decode
json_baz_d = {
    "baz_name": "name", 
    "baz_foo": [{"foo_name": "one"}, {"foo_name": "two"}], 
    "baz_bar":[{"bar_name": "first"}]
}
baz_d = Baz(**json_baz_d) # back to class instance
print(baz_d)
# Baz(baz_name='name', baz_foo=[{'foo_name': 'one'}, {'foo_name': 'two'}], baz_bar=[{'bar_name': 'first'}])

预期:

# encode
baz_e = Baz("name", [{"FOO NAME": "one"}, {"FOO NAME": "two"}], [{"barName": "first"}])
json_baz_e = json.dumps(asdict(baz_e))


# decode
json_baz_d = {
    "B A Z": "name", 
    "BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}], 
    "BAZ BAR":[{"barName": "first"}]
}
baz_d = Baz(**json_baz_d) # back to class instance

是唯一的解决方案dataclasses-json,还是没有额外的库仍有可能?

【问题讨论】:

    标签: python json python-dataclasses


    【解决方案1】:

    您当然可以为此使用 dataclasses-json,但是如果您不需要 marshmallow 架构的优势,您可以使用像 dataclass-wizard 这样的替代解决方案,即类似地,构建在数据类之上的 JSON 序列化库。它支持此处需要的 alias 字段映射;另一个好处是它在 Python stdlib 之外没有任何依赖项,除了 Python typing-extensions 模块。

    有一个 few choices 可用于指定 alias 字段映射,但在下面的示例中,我选择了两个选项来说明:

    • json_field,可以认为是dataclasses.field的别名
    • 可以在元配置中为数据类指定的 json_key_to_field 映射
    from dataclasses import dataclass
    from typing import List
    
    from dataclass_wizard import JSONWizard, json_field
    
    
    @dataclass
    class Foo:
        # pass all=True, so reverse mapping (field -> JSON) is also added
        foo_name: str = json_field('FOO NAME', all=True)
    
    
    @dataclass
    class Bar:
        # default key transform is `camelCase`, so alias is not needed here
        bar_name: str
    
    
    @dataclass
    class Baz(JSONWizard):
    
        class _(JSONWizard.Meta):
            json_key_to_field = {
                # Pass '__all__', so reverse mapping (field -> JSON) is also added
                '__all__': True,
                'B A Z': 'baz_name',
                'BAZ FOO': 'baz_foo',
                'BAZ BAR': 'baz_bar'
            }
    
        baz_name: str
        baz_foo: List[Foo]
        baz_bar: List[Bar]
    
    
    # encode
    baz_e = Baz("name", [Foo('one'), Foo('two')], [Bar('first')])
    json_baz_d = baz_e.to_dict()
    
    print(json_baz_d)
    # {'B A Z': 'name', 'BAZ FOO': [{'FOO NAME': 'one'}, {'FOO NAME': 'two'}], 'BAZ BAR': [{'barName': 'first'}]}
    
    # decode
    baz_d = Baz.from_dict(json_baz_d)  # back to class instance
    
    print(repr(baz_d))
    # > Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
    
    # True
    assert baz_e == baz_d
    

    NB:我注意到我想指出的一件显而易见的事情,因为它似乎不会导致预期的行为。在上面的问题中,您似乎正在实例化一个 Baz 实例,如下所示:

    baz_e = Baz("name", [{"foo_name": "one"}, {"foo_name": "two"}], [{"bar_name": "first"}])
    

    但是,请注意,在这种情况下,baz_foo 字段的值是 Python dict 对象的列表,而不是 Foo 实例的列表。为了解决这个问题,在上述解决方案中,我将{"foo_name": "one"} 更改为Foo('one')

    【讨论】:

    • 除了安装一个额外的库和限制版本之外,对于一个大型项目,例如大量的类和许多属性(如我的示例),您的映射将变成一个复杂的解决方案。我把最后一部分写得很好——应要求。 :) 。如果没有更优雅的解决方案,复杂是可以接受的。
    • 不幸的是,我不知道在 Python 中使用内置模块的优雅解决方案。我知道有一些很棒的验证库,比如pydantic,但即使是那些也需要你使用BaseModel 或他们自己版本的@dataclass 装饰器,并且还使用Field 定义别名映射。在这方面,我觉得这本身就是一个非常优雅的解决方案,但是可能有另一个库的更简单的解决方案,或者我自己没有想到的更好的方法。
    • 另外,请注意 Baz(**json_baz_d) 在上面的原始示例中不起作用,除非您使用像 pydantic 这样的模型,它覆盖了构造方法;原因是这似乎是一个嵌套的数据类模型,不幸的是,内部的 dict 对象默认不会被转换。
    • 我优雅地认为它是干净的代码,可以很容易地应用于更复杂的项目,即更大的项目,因为您提出的问题只是问题的简单介绍。没错,其中一种解决方案就是@dataclass_json装饰器和field(metadata=config(field_name ="overriddenGivenName"))已经提到了。
    • @MilovanTomašević 如果可能,您能否详细说明问题的更大示例是什么?如果有很多数据类字段并且 key transform 是非常确定的(例如,snake_case 到 TitleCase),那么可能有一种方法可以指定这样的转换函数并避免定义字段 alias 本例中的映射。
    【解决方案2】:

    dataclasses-json 的解决方案可能使代码更具可读性和更清晰。

    pip install dataclasses-json
    

    这个library 提供了一个简单的API,用于对dataclasses 与JSON 进行编码和解码。

    import json
    from typing import List
    from dataclasses import dataclass, asdict, field
    from dataclasses_json import config, dataclass_json
    
    
    @dataclass_json
    @dataclass
    class Foo:
        foo_name: str = field(metadata=config(field_name="FOO NAME")) # foo_name -> FOO NAME
    
    
    @dataclass_json
    @dataclass
    class Bar:
        bar_name: str = field(metadata=config(field_name="barName")) # bar_name -> barName
    
    
    @dataclass_json
    @dataclass
    class Baz:
        baz_name: str = field(metadata=config(field_name="B A Z")) # baz_name -> B A Z
        baz_foo: List[Foo] = field(metadata=config(field_name="BAZ FOO")) # baz_foo -> BAZ FOO
        baz_bar: List[Bar] = field(metadata=config(field_name="BAZ BAR")) # baz_bar -> BAZ BAR
    
    
    # encode
    baz_e = Baz("name", [{"FOO NAME": "one"}, {"FOO NAME": "two"}], [{"barName": "first"}])
    print(baz_e.to_dict())
    # {'B A Z': 'name', 'BAZ FOO': [{'FOO NAME': 'one'}, {'FOO NAME': 'two'}], 'BAZ BAR': [{'barName': 'first'}]}
    
    
    # decode
    json_baz_d = {
        "B A Z": "name", 
        "BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}], 
        "BAZ BAR":[{"barName": "first"}]
    }
    baz_d = Baz.from_dict(json_baz_d) # back to class instance
    print(baz_d)
    # Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
    
    
    # Mini test
    test_from_to = Baz.from_json(baz_e.to_json())
    print(test_from_to)
    # Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
    
    test_to_from = Baz.to_json(test_from_to)
    print(test_to_from)
    # {"B A Z": "name", "BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}], "BAZ BAR": [{"barName": "first"}]}
    

    从 camelCase(或 kebab-case)编码或解码?

    • JSON 字母大小写约定为 camelCase,在 Python 中成员约定为snake_case。

    • 您可以将其配置为在类级别和字段级别从其他套管方案进行编码/解码。

    from dataclasses import dataclass, field
    from dataclasses_json import LetterCase, config, dataclass_json
    
    
    # changing casing at the class level
    @dataclass_json(letter_case=LetterCase.CAMEL)
    @dataclass
    class Foo:
        foo_bar: str
        foo_baz: str
    
    
    f = Foo('one', 'two').to_json() 
    print(f)
    # {"fooBar": "one", "fooBaz": "two"}
    
    
    # at the field level
    @dataclass_json
    @dataclass
    class Foo:
        foo_bar: str = field(metadata=config(letter_case=LetterCase.CAMEL))
        foo_baz: str
    
    
    f = Foo('one', 'two').to_json() 
    print(f)
    # {"fooBar": "one", "foo_baz": "two"}
    
    ff = Foo.from_json(f)
    print(ff)
    # Foo(foo_bar='one', foo_baz='two')
    

    注意:

    • 如果出现错误:
    ImportError: cannot import name '_TypedDictMeta' from 'typing_extensions'
    

    您可能有旧版本的打字扩展,有必要将其更新到最新版本。

    pip install typing-extensions -U 
    

    【讨论】:

      猜你喜欢
      • 2012-06-17
      • 1970-01-01
      • 1970-01-01
      • 2016-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-06
      相关资源
      最近更新 更多