获取嵌套TypedDicts的键

huangapple go评论101阅读模式
英文:

Get keys of nested TypedDicts

问题

以下是已翻译的内容:

拥有以下的 TypedDict:

class MySubClass(TypedDict):
    name: str
    color: str

class MyClass(TypedDict):
    x: MySubClass
    y: str

有一个能够递归提取键的函数是什么:

[x_name, x_color, y]

该函数应该是动态的,以便可以提取所有种类的嵌套结构,但一层嵌套已经足够了。

非常感谢!

英文:

Having the following TypedDict:

class MySubClass(TypedDict):
    name: str
    color: str

class MyClass(TypedDict):
    x: MySubClass
    y: str

What is the function, that can extract the keys recursively like this:

[x_name, x_color, y]

The function should be dynamic so that it can extract all kinds of nested structures, but one level of nesting is already enough.

Many Thanks!

答案1

得分: 3

以下是您要翻译的内容:

Python >=3.10

对于 Python >=3.10,我们有typing.is_typeddict函数,可以用来检查是否实际上是TypedDict的子类型。

我们可以在TypedDict类上使用typing.get_type_hints(Python >=3.9)来获取其上定义的键和相应的类型。 (这可能比直接使用其__annotations__字典更好,正如@chepner在评论中指出的。)

一个简单的递归函数以您想要的方式获取键可能如下所示:

from typing import is_typeddict


def get_typed_dict_keys(cls: type) -> list[str]:
    keys: list[str] = []
    for key, type_ in get_type_hints(cls).items():
        if is_typeddict(type_):
            keys.extend(
                f"{key}_{sub_key}"
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

演示:

from typing import TypedDict


class Foo(TypedDict):
    name: str
    color: str


class Bar(TypedDict):
    x: Foo
    y: str


class Baz(TypedDict):
    a: int
    b: Bar


print(get_typed_dict_keys(Baz))

输出:['a', 'b_x_name', 'b_x_color', 'b_y']

显然,您可能会遇到嵌套键的名称冲突。但我相信您已经意识到了这一点。

附注:如果您使用的是Python >=3.11,并且有一些用NotRequired注释的键,这个解决方案仍然可以工作,因为get_type_hints会将这些注释解析为基础类型。

例如,以下Baz类:

from typing import TypedDict, NotRequired

...

class Baz(TypedDict):
    a: int
    b: NotRequired[Bar]

该函数仍然可以工作并返回相同的输出。


Python 3.9

在这里,我们需要创造性地解决问题,因为我们无法使用is_typeddict。为了毫不羞耻地从Pydantic中窃取一些内容,我们可以简单地检查某个对象是否1) 是dict的子类型并且2) 具有典型的TypedDict类属性。这显然不够可靠,但在大多数情况下应该足够好:

from typing import get_type_hints


def is_typeddict(cls: type) -> bool:
    if not (isinstance(cls, type) and issubclass(cls, dict)):
        return False
    return hasattr(cls, "__total__") and hasattr(cls, "__required_keys__")


def get_typed_dict_keys(cls: type) -> list[str]:
    keys: list[str] = []
    for key, type_ in get_type_hints(cls).items():
        if is_typeddict(type_):
            keys.extend(
                f"{key}_{sub_key}"
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

同样的演示,同样的输出。


Python 3.8

没有typing.get_type_hints,我们可以在get_typed_dict_keys函数中替换那个调用为cls.__annotations__

此外,TypedDict.__required_keys__类属性仅在Python 3.9中添加,因此要查看某个对象是否是TypedDict,我们只能检查__total__。这显然更不稳健,但在Python 3.8中是我们能做的最好的了。

适当调整类型注释等,3.8代码如下所示:

from typing import Any, List, Type


def is_typeddict(cls: Type[Any]) -> bool:
    if not (isinstance(cls, type) and issubclass(cls, dict)):
        return False
    return hasattr(cls, "__total__")


def get_typed_dict_keys(cls: Type[Any]) -> List[str]:
    keys: List[str] = []
    for key, type_ in cls.__annotations__.items():
        if is_typeddict(type_):
            keys.extend(
                f"{key}_{sub_key}"
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

同样的演示,同样的输出。


附注,仅供娱乐

以下是一个函数,可以返回嵌套TypedDict的注释的嵌套字典,并可选择将其展平,只是因为有趣:

from typing import Any, get_type_hints, is_typeddict


def get_typed_dict_annotations(
    cls: type,
    recursive: bool = False,
    flatten: bool = False,
) -> dict[str, Any]:
    if not recursive:
        return get_type_hints(cls)
    output: dict[str, Any] = {}
    for key, type_ in get_type_hints(cls).items():
        if not is_typeddict(type_):
            output[key] = type_
            continue
        sub_dict = get_typed_dict_annotations(
            type_,
            recursive=recursive,
            flatten=flatten,
        )
        if flatten:
            for sub_key, sub_type in sub_dict.items():
                output[f"{key}_{sub_key}"] = sub_type
        else:
            output[key] = sub_dict
    return output

演示:

from pprint import pprint
from typing import TypedDict


class Foo(TypedDict):
    name: str
    color: str


class Bar(TypedDict):
    x: Foo
    y: str


class Baz(TypedDict):
    a: int
    b: Bar


baz_annotations = get_typed_dict_annotations(Baz, recursive=True, flatten=True)
pprint(baz_annotations, sort_dicts=False)

print(list(baz_annotations.keys()))

baz_annotations = get_typed_dict_annotations(Baz, recursive=True)
pprint(baz_annotations, sort_dicts=False)

输出:

{'a': <class 'int'>,
 'b_x_name': <class 'str'>,
 'b_x_color': <class 'str'>,
 '

<details>
<summary>英文:</summary>

## Python `&gt;=3.10`

For Python `&gt;=3.10` we have the [`typing.is_typeddict`][1] function that we can use to check, if something is in fact a [`TypedDict`][2] subtype.

We can use [`typing.get_type_hints`][3] (Python `&gt;=3.9`) on a `TypedDict` class to get the keys and corresponding types defined on it. (This is likely better that using its [`__annotations__`][4] dictionary directly as pointed out by @chepner in a comment.)

A simple recursive function to get the keys the way you wanted might look like this:

```python
from typing import is_typeddict


def get_typed_dict_keys(cls: type) -&gt; list[str]:
    keys: list[str] = []
    for key, type_ in get_type_hints(cls).items():
        if is_typeddict(type_):
            keys.extend(
                f&quot;{key}_{sub_key}&quot;
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

Demo:

from typing import TypedDict


class Foo(TypedDict):
    name: str
    color: str


class Bar(TypedDict):
    x: Foo
    y: str


class Baz(TypedDict):
    a: int
    b: Bar


print(get_typed_dict_keys(Baz))

Output: [&#39;a&#39;, &#39;b_x_name&#39;, &#39;b_x_color&#39;, &#39;b_y&#39;]

Obviously, you might run into name collisions with the way the nested keys are formed. But I am sure you are aware of that.

Side note: If you are on Python &gt;=3.11 and have some keys that are annotated with NotRequired, this solution should still work because get_type_hints resolves those annotations to the underlying type.

E.g. the following Baz class:

from typing import TypedDict, NotRequired

...

class Baz(TypedDict):
    a: int
    b: NotRequired[Bar]

The function would still work and return the same output.


Python 3.9

Here we need to get creative because is_typeddict is not available to us. To shamelessly steal bits from Pydantic, we could simply check if something is 1) a dict subtype and 2) has the typical TypedDict class attributes. This is obviously less reliable, but should work well enough in most cases:

from typing import get_type_hints


def is_typeddict(cls: type) -&gt; bool:
    if not (isinstance(cls, type) and issubclass(cls, dict)):
        return False
    return hasattr(cls, &quot;__total__&quot;) and hasattr(cls, &quot;__required_keys__&quot;)


def get_typed_dict_keys(cls: type) -&gt; list[str]:
    keys: list[str] = []
    for key, type_ in get_type_hints(cls).items():
        if is_typeddict(type_):
            keys.extend(
                f&quot;{key}_{sub_key}&quot;
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

Same Demo, same output.


Python 3.8

Without typing.get_type_hints, we can just use replace that call in the get_typed_dict_keys function with cls.__annotations__.

Also, the TypedDict.__required_keys__ class attribute was only added in Python 3.9, so to see if something is a TypedDict, we can only check for __total__. This is obviously even less robust, but the best we can do with Python 3.8.

With type annotations etc. adjusted properly, the 3.8 code would look like this:

from typing import Any, List, Type


def is_typeddict(cls: Type[Any]) -&gt; bool:
    if not (isinstance(cls, type) and issubclass(cls, dict)):
        return False
    return hasattr(cls, &quot;__total__&quot;)


def get_typed_dict_keys(cls: Type[Any]) -&gt; List[str]:
    keys: List[str] = []
    for key, type_ in cls.__annotations__.items():
        if is_typeddict(type_):
            keys.extend(
                f&quot;{key}_{sub_key}&quot;
                for sub_key in get_typed_dict_keys(type_)
            )
        else:
            keys.append(key)
    return keys

Same Demo, same output.


PS, just for fun

Here is a function that can return a nested dictionary of annotations of nested TypedDicts and optionally flatten it, just because:

from typing import Any, get_type_hints, is_typeddict


def get_typed_dict_annotations(
    cls: type,
    recursive: bool = False,
    flatten: bool = False,
) -&gt; dict[str, Any]:
    if not recursive:
        return get_type_hints(cls)
    output: dict[str, Any] = {}
    for key, type_ in get_type_hints(cls).items():
        if not is_typeddict(type_):
            output[key] = type_
            continue
        sub_dict = get_typed_dict_annotations(
            type_,
            recursive=recursive,
            flatten=flatten,
        )
        if flatten:
            for sub_key, sub_type in sub_dict.items():
                output[f&quot;{key}_{sub_key}&quot;] = sub_type
        else:
            output[key] = sub_dict
    return output

Demo:

from pprint import pprint
from typing import TypedDict


class Foo(TypedDict):
    name: str
    color: str


class Bar(TypedDict):
    x: Foo
    y: str


class Baz(TypedDict):
    a: int
    b: Bar


baz_annotations = get_typed_dict_annotations(Baz, recursive=True, flatten=True)
pprint(baz_annotations, sort_dicts=False)

print(list(baz_annotations.keys()))

baz_annotations = get_typed_dict_annotations(Baz, recursive=True)
pprint(baz_annotations, sort_dicts=False)

Output:

{&#39;a&#39;: &lt;class &#39;int&#39;&gt;,
 &#39;b_x_name&#39;: &lt;class &#39;str&#39;&gt;,
 &#39;b_x_color&#39;: &lt;class &#39;str&#39;&gt;,
 &#39;b_y&#39;: &lt;class &#39;str&#39;&gt;}
[&#39;a&#39;, &#39;b_x_name&#39;, &#39;b_x_color&#39;, &#39;b_y&#39;]
{&#39;a&#39;: &lt;class &#39;int&#39;&gt;,
 &#39;b&#39;: {&#39;x&#39;: {&#39;name&#39;: &lt;class &#39;str&#39;&gt;, &#39;color&#39;: &lt;class &#39;str&#39;&gt;},
       &#39;y&#39;: &lt;class &#39;str&#39;&gt;}}

huangapple
  • 本文由 发表于 2023年6月16日 02:18:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76484484.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定