获取嵌套TypedDicts的键

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

Get keys of nested TypedDicts

问题

以下是已翻译的内容:

拥有以下的 TypedDict:

  1. class MySubClass(TypedDict):
  2. name: str
  3. color: str
  4. class MyClass(TypedDict):
  5. x: MySubClass
  6. y: str

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

  1. [x_name, x_color, y]

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

非常感谢!

英文:

Having the following TypedDict:

  1. class MySubClass(TypedDict):
  2. name: str
  3. color: str
  4. class MyClass(TypedDict):
  5. x: MySubClass
  6. y: str

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

  1. [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在评论中指出的。)

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

  1. from typing import is_typeddict
  2. def get_typed_dict_keys(cls: type) -> list[str]:
  3. keys: list[str] = []
  4. for key, type_ in get_type_hints(cls).items():
  5. if is_typeddict(type_):
  6. keys.extend(
  7. f"{key}_{sub_key}"
  8. for sub_key in get_typed_dict_keys(type_)
  9. )
  10. else:
  11. keys.append(key)
  12. return keys

演示:

  1. from typing import TypedDict
  2. class Foo(TypedDict):
  3. name: str
  4. color: str
  5. class Bar(TypedDict):
  6. x: Foo
  7. y: str
  8. class Baz(TypedDict):
  9. a: int
  10. b: Bar
  11. print(get_typed_dict_keys(Baz))

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

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

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

例如,以下Baz类:

  1. from typing import TypedDict, NotRequired
  2. ...
  3. class Baz(TypedDict):
  4. a: int
  5. b: NotRequired[Bar]

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


Python 3.9

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

  1. from typing import get_type_hints
  2. def is_typeddict(cls: type) -> bool:
  3. if not (isinstance(cls, type) and issubclass(cls, dict)):
  4. return False
  5. return hasattr(cls, "__total__") and hasattr(cls, "__required_keys__")
  6. def get_typed_dict_keys(cls: type) -> list[str]:
  7. keys: list[str] = []
  8. for key, type_ in get_type_hints(cls).items():
  9. if is_typeddict(type_):
  10. keys.extend(
  11. f"{key}_{sub_key}"
  12. for sub_key in get_typed_dict_keys(type_)
  13. )
  14. else:
  15. keys.append(key)
  16. 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代码如下所示:

  1. from typing import Any, List, Type
  2. def is_typeddict(cls: Type[Any]) -> bool:
  3. if not (isinstance(cls, type) and issubclass(cls, dict)):
  4. return False
  5. return hasattr(cls, "__total__")
  6. def get_typed_dict_keys(cls: Type[Any]) -> List[str]:
  7. keys: List[str] = []
  8. for key, type_ in cls.__annotations__.items():
  9. if is_typeddict(type_):
  10. keys.extend(
  11. f"{key}_{sub_key}"
  12. for sub_key in get_typed_dict_keys(type_)
  13. )
  14. else:
  15. keys.append(key)
  16. return keys

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


附注,仅供娱乐

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

  1. from typing import Any, get_type_hints, is_typeddict
  2. def get_typed_dict_annotations(
  3. cls: type,
  4. recursive: bool = False,
  5. flatten: bool = False,
  6. ) -> dict[str, Any]:
  7. if not recursive:
  8. return get_type_hints(cls)
  9. output: dict[str, Any] = {}
  10. for key, type_ in get_type_hints(cls).items():
  11. if not is_typeddict(type_):
  12. output[key] = type_
  13. continue
  14. sub_dict = get_typed_dict_annotations(
  15. type_,
  16. recursive=recursive,
  17. flatten=flatten,
  18. )
  19. if flatten:
  20. for sub_key, sub_type in sub_dict.items():
  21. output[f"{key}_{sub_key}"] = sub_type
  22. else:
  23. output[key] = sub_dict
  24. return output

演示:

  1. from pprint import pprint
  2. from typing import TypedDict
  3. class Foo(TypedDict):
  4. name: str
  5. color: str
  6. class Bar(TypedDict):
  7. x: Foo
  8. y: str
  9. class Baz(TypedDict):
  10. a: int
  11. b: Bar
  12. baz_annotations = get_typed_dict_annotations(Baz, recursive=True, flatten=True)
  13. pprint(baz_annotations, sort_dicts=False)
  14. print(list(baz_annotations.keys()))
  15. baz_annotations = get_typed_dict_annotations(Baz, recursive=True)
  16. pprint(baz_annotations, sort_dicts=False)

输出:

  1. {'a': <class 'int'>,
  2. 'b_x_name': <class 'str'>,
  3. 'b_x_color': <class 'str'>,
  4. '
  5. <details>
  6. <summary>英文:</summary>
  7. ## Python `&gt;=3.10`
  8. 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.
  9. 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.)
  10. A simple recursive function to get the keys the way you wanted might look like this:
  11. ```python
  12. from typing import is_typeddict
  13. def get_typed_dict_keys(cls: type) -&gt; list[str]:
  14. keys: list[str] = []
  15. for key, type_ in get_type_hints(cls).items():
  16. if is_typeddict(type_):
  17. keys.extend(
  18. f&quot;{key}_{sub_key}&quot;
  19. for sub_key in get_typed_dict_keys(type_)
  20. )
  21. else:
  22. keys.append(key)
  23. return keys

Demo:

  1. from typing import TypedDict
  2. class Foo(TypedDict):
  3. name: str
  4. color: str
  5. class Bar(TypedDict):
  6. x: Foo
  7. y: str
  8. class Baz(TypedDict):
  9. a: int
  10. b: Bar
  11. 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:

  1. from typing import TypedDict, NotRequired
  2. ...
  3. class Baz(TypedDict):
  4. a: int
  5. 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:

  1. from typing import get_type_hints
  2. def is_typeddict(cls: type) -&gt; bool:
  3. if not (isinstance(cls, type) and issubclass(cls, dict)):
  4. return False
  5. return hasattr(cls, &quot;__total__&quot;) and hasattr(cls, &quot;__required_keys__&quot;)
  6. def get_typed_dict_keys(cls: type) -&gt; list[str]:
  7. keys: list[str] = []
  8. for key, type_ in get_type_hints(cls).items():
  9. if is_typeddict(type_):
  10. keys.extend(
  11. f&quot;{key}_{sub_key}&quot;
  12. for sub_key in get_typed_dict_keys(type_)
  13. )
  14. else:
  15. keys.append(key)
  16. 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:

  1. from typing import Any, List, Type
  2. def is_typeddict(cls: Type[Any]) -&gt; bool:
  3. if not (isinstance(cls, type) and issubclass(cls, dict)):
  4. return False
  5. return hasattr(cls, &quot;__total__&quot;)
  6. def get_typed_dict_keys(cls: Type[Any]) -&gt; List[str]:
  7. keys: List[str] = []
  8. for key, type_ in cls.__annotations__.items():
  9. if is_typeddict(type_):
  10. keys.extend(
  11. f&quot;{key}_{sub_key}&quot;
  12. for sub_key in get_typed_dict_keys(type_)
  13. )
  14. else:
  15. keys.append(key)
  16. 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:

  1. from typing import Any, get_type_hints, is_typeddict
  2. def get_typed_dict_annotations(
  3. cls: type,
  4. recursive: bool = False,
  5. flatten: bool = False,
  6. ) -&gt; dict[str, Any]:
  7. if not recursive:
  8. return get_type_hints(cls)
  9. output: dict[str, Any] = {}
  10. for key, type_ in get_type_hints(cls).items():
  11. if not is_typeddict(type_):
  12. output[key] = type_
  13. continue
  14. sub_dict = get_typed_dict_annotations(
  15. type_,
  16. recursive=recursive,
  17. flatten=flatten,
  18. )
  19. if flatten:
  20. for sub_key, sub_type in sub_dict.items():
  21. output[f&quot;{key}_{sub_key}&quot;] = sub_type
  22. else:
  23. output[key] = sub_dict
  24. return output

Demo:

  1. from pprint import pprint
  2. from typing import TypedDict
  3. class Foo(TypedDict):
  4. name: str
  5. color: str
  6. class Bar(TypedDict):
  7. x: Foo
  8. y: str
  9. class Baz(TypedDict):
  10. a: int
  11. b: Bar
  12. baz_annotations = get_typed_dict_annotations(Baz, recursive=True, flatten=True)
  13. pprint(baz_annotations, sort_dicts=False)
  14. print(list(baz_annotations.keys()))
  15. baz_annotations = get_typed_dict_annotations(Baz, recursive=True)
  16. pprint(baz_annotations, sort_dicts=False)

Output:

  1. {&#39;a&#39;: &lt;class &#39;int&#39;&gt;,
  2. &#39;b_x_name&#39;: &lt;class &#39;str&#39;&gt;,
  3. &#39;b_x_color&#39;: &lt;class &#39;str&#39;&gt;,
  4. &#39;b_y&#39;: &lt;class &#39;str&#39;&gt;}
  1. [&#39;a&#39;, &#39;b_x_name&#39;, &#39;b_x_color&#39;, &#39;b_y&#39;]
  1. {&#39;a&#39;: &lt;class &#39;int&#39;&gt;,
  2. &#39;b&#39;: {&#39;x&#39;: {&#39;name&#39;: &lt;class &#39;str&#39;&gt;, &#39;color&#39;: &lt;class &#39;str&#39;&gt;},
  3. &#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:

确定