使用双星运算符从字典中命名的元组:嵌套字段也被解包了吗?

named tuple from dictionary using double-star-operator: are nested fields unpacked too?

我有两个 classes:TopNested,要创建它们我需要提供 TopDefinitionNestedDefinition 对象,属于 NamedTuple 类型(类型注释需要定义)。 Class Top 包含属性,它是 Nested 实例对象的列表。

有一个嵌套的字典,用于创建命名元组的实例。 输入字典 item 如下所示:

type =<class 'dict'>
value={'t1': 'qwe', 't2': 'QWE', 't3': [{'n1': 'aaa', 'n2': 1}, {'n1': 'bb', 'n2': 3}]} 

然后用代码

解压创建 class TopDefinition 的实例

q = Top(top=TopDefinition(**item)) 用作创建 class Top 实例的输入。这很好用,稍后我可以在 q class 中看到输入参数的类型和值:

type=<class '__main__.TopDefinition'>
value=TopDefinition(t1='qwe', t2='QWE', t3=[{'n1': 'aaa', 'n2': 1}, {'n1': 'bb', 'n2': 3}])

TopDefinition 实例已正确创建为具有字段的命名元组:t1、t2、t3。

问题是:t3是什么类型的?
它是字典列表还是命名元组列表(隐式转换,因为它在 TopDefinition 中定义为 List[NestedTuple]?
输出表明这是一个字典列表,因为当我遍历 t3 并显示类型和值时,我看到:

type=<class 'dict'>,
value={'n1': 'aaa', 'n2': 1}
Is named_tuple=False  

然后我用 ** 解压缩 {'n1': 'aaa', 'n2': 1} 以创建 NestedDefinition 实例,它工作正常,所以它应该是一个字典。
另一方面,mypy(带有选项 --ignore-missing-imports --strict)说 error: Argument after ** must be a mapping 这对我来说意味着它不是字典。

运行 的完整代码如下:

"""Replicate the problem."""
from typing import Any, List, NamedTuple


class NestedDefinition(NamedTuple):
    """Nested object metadata for mypy type annotation."""

    n1: str
    n2: int


class TopDefinition(NamedTuple):
    """Top object metadata for mypy type annotation."""

    t1: str
    t2: str
    t3: List[NestedDefinition]


def isnamedtupleinstance(x: Any) -> bool:
    """Check if object is named tuple."""
    t = type(x)
    b = t.__bases__
    print("-------{}".format(b))
    if len(b) != 1 or b[0] != tuple:
        return False
    f = getattr(t, '_fields', None)
    if not isinstance(f, tuple):
        return False
    return all(type(n) == str for n in f)


class Nested:
    """Nested object."""

    n1: str
    n2: int

    def __init__(self, nested: NestedDefinition) -> None:
        print("{cName} got:\n\ttype={y}\n\tvalue={v}\n\tIS named_tuple: {b}".format(
            cName=type(self).__name__, y=type(nested), v=nested, b=isnamedtupleinstance(nested)))
        self.n1 = nested.n1
        self.n2 = nested.n2


class Top:
    """Top object."""

    t1: str
    t2: str
    t3: List[Nested]

    def __init__(self, top: TopDefinition) -> None:
        print("{cName} got:\n\ttype={y}\n\tvalue={v}".format(cName=type(self).__name__,
                                                             y=type(top), v=top))

        self.t1 = top.t1
        self.t2 = top.t2
        self.t3 = []
        if top.t3:
            for sub_item in top.t3:
                print("Nested passing:\n\ttype={t},\n\tvalue={v}\n\tIs named_tuple={b}".format(
                    t=type(sub_item), v=sub_item, b=isnamedtupleinstance(sub_item)))
                nested = Nested(nested=NestedDefinition(**sub_item))
                self.addNestedObj(nested)

    def addNestedObj(self, nested: Nested) -> None:
        """Append nested object to array in top object."""
        self.t3.append(nested)


def build_data_structure(someDict: List) -> None:
    """Replicate problem."""
    for item in someDict:
        print("Top passing:\n\ttype ={type}\n\tvalue={value}".format(
            type=type(item), value=item))
        w = Top(top=TopDefinition(**item))


x = [
    {
        't1': 'qwe',
        't2': 'QWE',
        't3': [
            {'n1': 'aaa', 'n2': 1},
            {'n1': 'bb', 'n2': 3}
        ]
    },
    {
        't1': 'asd',
        't2': 'ASD',
        't3': [
            {'n1': 'cc', 'n2': 7},
            {'n1': 'dd', 'n2': 9}
        ]
    }
]


build_data_structure(someDict=x)

类型提示用于静态类型检查。它们不会影响运行时行为。

调用中的**mapping语法只扩展顶级键值对;就好像你打电话给

TopDefinition(t1='qwe', t2='QWE', t3=[{'n1': 'aaa', 'n2': 1}, {'n1': 'bb', 'n2': 3}])

调用的对象没有给出任何关于这些关键字参数来源的信息; namedtuple class __new__ 方法不关心也不关心关键字参数是如何设置的。

因此列表保持不变,没有为您转换。您必须预先 :

def build_data_structure(someDict: List[Mapping]) -> None:
    for item in someDict:
        print("Top passing:\n\ttype ={type}\n\tvalue={value}".format(
            type=type(item), value=item))

        t3updated = []
        for nested in item['t3']:
            if not isinstance(nested, NestedDefinition):
                nested = NestedDefinition(**nested)
            t3updated.append(nested)
        item['t3'] = t3updated
        w = Top(top=TopDefinition(**item))

因为您使用了 **mapping 调用,mypy 等静态类型分析器无法确定您的列表与 List[NestedDefinition] 类型提示不匹配,也不会提醒您,但是如果你像我上面那样使用单独的参数显式地使用完整调用,那么你会收到一条错误消息,告诉你你没有使用正确的类型。

在 mypy 中,您还可以使用 TypedDict type definition 来记录传递给 build_data_structure() 的列表包含什么类型的映射,此时 mypy 可以推断出您的 t3 值是字典列表,而不是命名元组列表。

接下来,mypy 给你的 error: Argument after ** must be a mapping 错误是基于 mypy 可以访问的 类型提示 ,而不是关于运行时信息。你的循环:

for sub_item in top.t3:

告诉mypy正确的代码中,sub_item必须是一个NestedDefinition对象,因为 t3: List[NestedDefinition] 是这样告诉它的。 NestedDefinition 对象不是映射,因此 sub_item 引用不能用于 **mapping 调用。

您通过 build_data_structure() 中的不透明 TopDefinition(**item) 调用(其中那些 item 对象来自不合格的 List)偷偷进入了一些实际映射的事实既不是在这里或那里; mypy 不知道 item 是什么类型的对象,因此也无法对这些值做出任何断言。