英文:
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 `>=3.10`
For Python `>=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 `>=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) -> 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
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: ['a', 'b_x_name', 'b_x_color', 'b_y']
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 >=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) -> 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
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]) -> 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
Same Demo, same output.
PS, just for fun
Here is a function that can return a nested dictionary of annotations of nested TypedDict
s 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,
) -> 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
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:
{'a': <class 'int'>,
'b_x_name': <class 'str'>,
'b_x_color': <class 'str'>,
'b_y': <class 'str'>}
['a', 'b_x_name', 'b_x_color', 'b_y']
{'a': <class 'int'>,
'b': {'x': {'name': <class 'str'>, 'color': <class 'str'>},
'y': <class 'str'>}}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论