在具有通用类型的 class 中定义的数据 class 的类型提示

Type hints for dataclass defined inside a class with generic types

我知道标题很混乱,所以我以二叉搜索树为例:

使用普通的class定义

# This code passed mypy test
from typing import Generic, TypeVar

T = TypeVar('T')
class BST(Generic[T]):
    class Node:        
        def __init__(
            self,
            val: T,
            left: 'BST.Node',
            right: 'BST.Node'
        ) -> None:
            self.val = val
            self.left = left
            self.right = right

以上代码通过了mypy测试。

使用dataclass

但是,当我尝试使用dataclass来简化Node的定义时,代码在mypy测试中失败了。

# This code failed to pass mypy test
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar('T')
class BST(Generic[T]):
    @dataclass
    class Node:
        val: T
        left: 'BST.Node'
        right: 'BST.Node'

mypy 给了我这个错误信息:(test_typing.py:8val: T 行)

test_typing.py:8: error: Type variable "test_typing.T" is unbound
test_typing.py:8: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
test_typing.py:8: note: (Hint: Use "T" in function signature to bind "T" inside a function)

找出问题所在

# This code passed mypy test, suggest the problem is the reference to `T` in the dataclass definition
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar('T')
class BST(Generic[T]):
    @dataclass
    class Node:
        val: int # chose `int` just for testing
        left: 'BST.Node'
        right: 'BST.Node'

上面的代码再次通过了测试,所以我认为问题出在数据class定义中对T的引用。有谁知道将来如何解决这个问题以实现我最初的目标?

让我们从PEP 484中写的关于类型变量的作用域规则开始:

A generic class nested in another generic class cannot use same type variables. The scope of the type variables of the outer class doesn't cover the inner one:

T = TypeVar('T')
S = TypeVar('S')

class Outer(Generic[T]):
   class Bad(Iterable[T]):       # Error
       ...
   class AlsoBad:
       x = None  # type: List[T] # Also an error

   class Inner(Iterable[S]):     # OK
       ...
   attr = None  # type: Inner[T] # Also OK

这就是为什么带有嵌套装饰 class 的示例不起作用的原因。

现在让我们回答这个问题,为什么该示例使用带有 TypeVar 变量的 __init__ 函数。

这是因为方法 __init__ 被 mypy 视为具有独立 TypeVar 变量的泛型方法。例如 reveal_type(BST[int].Node.__init__) 显示 Revealed type is 'def [T, T] (self: main.BST.Node, val: T'-1, left: main.BST.Node, right: main.BST.Node)'。即 T 未绑定到 int 此处。

嵌套 classes 不能隐式使用包含 classes 的 TypeVar:嵌套 class 必须是 Generic 且未绑定 TypeVar.

BT = TypeVar('BT')
NT = TypeVar('NT')

class BST(Generic[BT]):
    root: 'BST.Node[BT]'  # root note is of same type as search tree

    @dataclass
    class Node(Generic[NT]):  # generic node may be of any type
        val: NT
        left: 'BST.Node[NT]'
        right: 'BST.Node[NT]'

这使得嵌套的 class well-defined 在其包含的 class 之外被引用。潜在的问题是嵌套的 class 与外部专家分开存在——推理只知道 BST.NodeBST.Node[T],而不是 BST[T].Node.


由于嵌套不提供任何功能优势,定义单独的 classes 重用相同的 TypeVar

通常更简单
T = TypeVar('T')

class BST(Generic[T]):
    root: 'Node[T]'

@dataclass
class Node(Generic[T]):
    val: T
    left: 'Node[T]'
    right: 'Node[T]'