如何创建一个处理具有不同名称的嵌套字典的数据类?

发布于 2025-01-11 15:58:24 字数 617 浏览 4 评论 0原文

我正在使用英雄联盟 API 中的数据来学习 Python、JSON 和数据类。我使用英安岩创建了父类和子类,允许使用以下语法访问数据:champs.data['Ahri']['key']。但是,我想知道是否有一种方法可以创建一个将键作为字段返回的类,以便可以使用以下语法访问数据:champs.data.Ahri.key

这是工作代码:

from dataclasses import dataclass
from dacite import from_dict

j1 = {'type': 'champion',
      'data': {'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
      'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}}}

@dataclass
class C:
    type: str
    data: dict

@dataclass
class P:
    type: str
    data: dict

champs = from_dict(data_class=P, data=j1)
champs.data['Ahri']['key']

I am using the data from the League of Legends API to learn Python, JSON, and Data Classes. Using dacite, I have created parent and child classes that allow access to the data using this syntax: champs.data['Ahri']['key']. However, I wonder if there is a way to create a class that returns the keys as fields so one could access the data using this syntax: champs.data.Ahri.key.

Here is the working code:

from dataclasses import dataclass
from dacite import from_dict

j1 = {'type': 'champion',
      'data': {'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
      'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}}}

@dataclass
class C:
    type: str
    data: dict

@dataclass
class P:
    type: str
    data: dict

champs = from_dict(data_class=P, data=j1)
champs.data['Ahri']['key']

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

记忆消瘦 2025-01-18 15:58:25

如果是我,我可能会留下/制作 champions 一本字典。 访问它

然后像 champions['Ahri'].key一样

import dataclasses

@dataclasses.dataclass
class Champion:
    id: str
    key: str
    name: str

j1 = {
    'type': 'champion',
    'data': {
        'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
        'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}
    }
}

champions = {
    champion["id"]: Champion(**champion)
    for champion in j1["data"].values()
}

print(champions['Ahri'].key)

:结果 103

但是,如果您真的热衷于 champions.Ahri.key > 然后你可以将 Champions 实现为一个空类,并

import dataclasses

@dataclasses.dataclass
class Champion:
    id: str
    key: str
    name: str

@dataclasses.dataclass
class Champions:
    pass

j1 = {
    'type': 'champion',
    'data': {
        'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
        'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}
    }
}

champions = Champions()
for champion in j1["data"].values():
    setattr(champions, champion["id"], Champion(**champion))

print(champions.Ahri.key)

再次使用 setattr() 给出 103

注意:@dataclass 装饰器可能会被省略来自冠军()。

If it were me, I would probably leave/make champions a dictionary. Then access it like champions['Ahri'].key

Something like:

import dataclasses

@dataclasses.dataclass
class Champion:
    id: str
    key: str
    name: str

j1 = {
    'type': 'champion',
    'data': {
        'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
        'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}
    }
}

champions = {
    champion["id"]: Champion(**champion)
    for champion in j1["data"].values()
}

print(champions['Ahri'].key)

resulting in 103

However if you were really keen on champions.Ahri.key then you can implement Champions as an empty class and use setattr()

import dataclasses

@dataclasses.dataclass
class Champion:
    id: str
    key: str
    name: str

@dataclasses.dataclass
class Champions:
    pass

j1 = {
    'type': 'champion',
    'data': {
        'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
        'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'}
    }
}

champions = Champions()
for champion in j1["data"].values():
    setattr(champions, champion["id"], Champion(**champion))

print(champions.Ahri.key)

again giving you 103

Note: The @dataclass decorator can likely be omitted from Champion().

风吹短裙飘 2025-01-18 15:58:25

您可能获得的最接近的结果 - 至少以足够安全的方式 - 正如@JonSG建议的,使用champs.data['Ahri'].key

这是一个使用 dataclass-wizard 的简单示例。它不像我所知的英安岩那样进行严格类型检查。

相反,它选择在可能的情况下进行隐式类型的强制转换,这在某些情况下很有用;您可以在下面看到一个示例 - 在本例中,将 str 转换为带注释的 int

注意:此示例应适用于包含 __future__ 导入的 Python 3.7+。

from __future__ import annotations

from dataclasses import dataclass
from dataclass_wizard import fromdict


data = {
    'type': 'champion',
    'data': {
          'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
          'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'},
    }
}


@dataclass
class P:
    type: str
    data: dict[str, Character]


@dataclass
class Character:
    id: str
    key: int
    name: str


champs = fromdict(P, data)

print(champs)
print(champs.data['Ahri'].key)

输出:

P(type='champion', data={'Aatrox': Character(id='Aatrox', key=266, name='Aatrox'), 'Ahri': Character(id='Ahri', key=103, name='Ahri')})
103

The closest you can probably get - at least in a safe enough manner - is as @JonSG suggests, using champs.data['Ahri'].key.

Here's a straightforward example using the dataclass-wizard. It doesn't do a strict type checking as I know dacite does.

Instead, it opts to do implicit type coercision where possible, which is useful in some cases; you can see an example of this below - str to annotated int in this case.

Note: This example should work for Python 3.7+ with the included __future__ import.

from __future__ import annotations

from dataclasses import dataclass
from dataclass_wizard import fromdict


data = {
    'type': 'champion',
    'data': {
          'Aatrox': {'id': 'Aatrox', 'key': '266', 'name': 'Aatrox'},
          'Ahri': {'id': 'Ahri', 'key': '103', 'name': 'Ahri'},
    }
}


@dataclass
class P:
    type: str
    data: dict[str, Character]


@dataclass
class Character:
    id: str
    key: int
    name: str


champs = fromdict(P, data)

print(champs)
print(champs.data['Ahri'].key)

Output:

P(type='champion', data={'Aatrox': Character(id='Aatrox', key=266, name='Aatrox'), 'Ahri': Character(id='Ahri', key=103, name='Ahri')})
103
当梦初醒 2025-01-18 15:58:25

如何做到这一点

d = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}


def dict_to_class(d) -> object:
    if isinstance(d, dict):

        class C:
            pass

        for k, v in d.items():
            setattr(C, k, dict_to_class(v))
        return C
    else:
        return d


champ = dict_to_class(d)

print(champ.data.Ahri.key)
# 103

这里的关键是 setatter 内置方法,它接受一个对象、一个字符串和一些值,并在该对象上创建一个属性(字段),根据字符串命名并包含值。

不要这样做!

我必须强调,这样做几乎没有充分的理由。当处理未知形状的 JSON 数据时,表示它的正确方法是一个dict

如果您确实知道数据的形状,则应该创建一个专门的数据类,如下所示:

from dataclasses import dataclass

d = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}

@dataclass
class Champion:
    id: str
    key: str
    name: str

champions = {name: Champion(**attributes) for name, attributes in d["data"].items()}

print(champions)
# {'Aatrox': Champion(id='Aatrox', key='266', name='Aatrox'), 'Ahri': Champion(id='Ahri', key='103', name='Ahri')}

print(champions["Aatrox"].key)
# 266

How to do this

d = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}


def dict_to_class(d) -> object:
    if isinstance(d, dict):

        class C:
            pass

        for k, v in d.items():
            setattr(C, k, dict_to_class(v))
        return C
    else:
        return d


champ = dict_to_class(d)

print(champ.data.Ahri.key)
# 103

The key here is the setatter builtin method, which takes an object, a string, and some value, and creates an attribute (field) on that object, named according to the string and containing the value.

Don't do this!

I must stress that there is almost never a good reason to do this. When dealing with JSON data of an unknown shape, the correct way to represent it is a dict.

If you do know the shape of the data, you should create a specialized dataclass, like so:

from dataclasses import dataclass

d = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}

@dataclass
class Champion:
    id: str
    key: str
    name: str

champions = {name: Champion(**attributes) for name, attributes in d["data"].items()}

print(champions)
# {'Aatrox': Champion(id='Aatrox', key='266', name='Aatrox'), 'Ahri': Champion(id='Ahri', key='103', name='Ahri')}

print(champions["Aatrox"].key)
# 266
梦过后 2025-01-18 15:58:25

英安岩文档有一个关于 嵌套结构 的部分,非常接近您想要的内容。他们逐字使用的示例如下:

@dataclass
class A:
    x: str
    y: int


@dataclass
class B:
    a: A


data = {
    'a': {
        'x': 'test',
        'y': 1,
    }
}

result = from_dict(data_class=B, data=data)

assert result == B(a=A(x='test', y=1))

我们可以访问任意深度的字段,例如 result.ax == 'test'

此数据与您的数据之间的关键区别在于 data 键下的字典具有具有任意值的键(AatroxAhri 等) 。英安岩未设置为动态创建新的字段名称,因此您将获得的最好结果类似于@JonSG 的后半部分 answer,使用setattr动态构建新字段。

不过,让我们想象一下您将如何使用这些数据。也许您希望某个点能够迭代您的冠军,以便执行过滤/转换等。手术。可以迭代python中的字段,但你必须真正深入研究python内部,这意味着你的代码会更少可读/一般可理解。

更好的方法是采用以下方法之一:

  1. j1 预处理为适合您要使用的结构的形状,然后使用英安岩和适合新结构的 dataclass。例如,将 data 字典的值拉出到列表中也许是有意义的。
  2. 使用英安岩分步进行处理。例如,如下所示:
from dataclasses import dataclass
from dacite import from_dict


@dataclass
class TopLevel:
    type: str
    data: dict


j1 = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}

champions = from_dict(data_class=TopLevel, data=j1)
# champions.data is a dict of dicts

@dataclass
class Champion:
    id: str
    key: str
    name: str


# transform champions.data into a dict of Champions
for k, v in champions.data.items():
    champions.data[k] = from_dict(data_class=Champion, data=v)

# now, you can do interesting things like the following filter operation
start_with_a = [
    champ for champ in champions.data.values() if champ.name.lower().startswith("a")
]
print(start_with_a)
# [Champion(id='Aatrox', key='266', name='Aatrox'), Champion(id='Ahri', key='103', name='Ahri')]

The dacite docs have a section about nested structures that is very close to what you want. The example they use, verbatim, is as follows:

@dataclass
class A:
    x: str
    y: int


@dataclass
class B:
    a: A


data = {
    'a': {
        'x': 'test',
        'y': 1,
    }
}

result = from_dict(data_class=B, data=data)

assert result == B(a=A(x='test', y=1))

We can access fields at arbitrary depth as e.g. result.a.x == 'test'.

The critical difference between this and your data is that the dictionary under the data key has keys with arbitrary values (Aatrox, Ahri, etc.). dacite isn't set up to create new field names on the fly, so the best you're going to get is something like the latter part of @JonSG's answer, which uses setattr to dynamically build new fields.

Let's imagine how you would use this data for a moment, though. Probably you'd want a some point to be able to iterate over your champions in order to perform a filter/transform/etc. operation. It's possible to iterate over fields in python, but you have to really dig into python internals, which means your code will be less readable/generally comprehensible.

Much better would be one of the following:

  1. Preprocess j1 into a shape that fits the structure you want to use, and then use dacite with a dataclass that fits the new structure. For example, maybe it makes sense to pull the values of the data dict out into a list.
  2. Process in steps using dacite. For example, something like the following:
from dataclasses import dataclass
from dacite import from_dict


@dataclass
class TopLevel:
    type: str
    data: dict


j1 = {
    "type": "champion",
    "data": {
        "Aatrox": {"id": "Aatrox", "key": "266", "name": "Aatrox"},
        "Ahri": {"id": "Ahri", "key": "103", "name": "Ahri"},
    },
}

champions = from_dict(data_class=TopLevel, data=j1)
# champions.data is a dict of dicts

@dataclass
class Champion:
    id: str
    key: str
    name: str


# transform champions.data into a dict of Champions
for k, v in champions.data.items():
    champions.data[k] = from_dict(data_class=Champion, data=v)

# now, you can do interesting things like the following filter operation
start_with_a = [
    champ for champ in champions.data.values() if champ.name.lower().startswith("a")
]
print(start_with_a)
# [Champion(id='Aatrox', key='266', name='Aatrox'), Champion(id='Ahri', key='103', name='Ahri')]
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文