存根文件中的Mypy泛型类



我正在考虑将类型存根添加到一个库中,该库有一个名为List的集合类,它本质上是内置list的包装器。出于所有实际目的,你可以假设它看起来是这样的:

# library.py
class List:
def __init__(self, *values):
self.values = values

现在,在我的存根文件library.pyi中,我有:

# library.pyi
from typing import Generic, TypeVar, Iterable
T = TypeVar('T')
class List(Generic[T]):
def __init__(self, *values: T) -> None: ...

如果我做了这样的事情,我希望打字失败:

# client.py
from library import List
def f() -> List[str]:
return List(*range(10))

但是mypy client.py以0退出。此外,CCD_ 5与CCD_。

我的理解是,类型提示对运行时没有任何影响。这显然是错误的。有人能纠正我关于类型提示如何工作的心理模型吗?

此外,有什么可以得到我想要的吗(即mypy client.py失败)?

为了了解发生了什么,我认为首先回顾一些背景材料会很有帮助。

在Python 3.0中,Python添加了一个新的语言功能,称为函数注释。函数注释本身与类型注释无关——它们只是将任意信息附加到函数的一种方式。

基本上,Python所做的就是获取包含的任何注释,对它们进行求值,然后将它们添加到函数的__annotations__字段中。例如,尝试运行以下代码:

def foo(x: 3 + 4 * 5, y: [i + 1 for i in range(4)]) -> max(3, 4):
pass
print(foo.__annotations__)

如果我们运行这个,我们将得到:

{'x': 23, 'y': [1, 2, 3, 4], 'return': 4}

也就是说,Python将运行3 + 4 * 5[i + 1 for i in range(4)]max(3, 4),然后将该数据附加到__annotations__。完成这些操作后,Python将不再执行其他操作。

简而言之,这意味着。。。

  1. Python仍然必须计算每个单独的注释,该注释必须是有效的Python表达式
  2. 但这样做之后,注释将被忽略

因此,这意味着当我们使用特定的类型提示时,每个类型提示都会在函数定义时单独求值/必须是有效的表达式,但随后会被Python运行时的后记忽略。

(需要注意的是,这种行为在未来可能会略有变化:因为我们必须评估每个注释,所以使用类型提示确实会带来轻微的性能损失——有人说可能会更改Python,所以在未来,表达式将作为字符串存储在__annotations__中,而不是立即进行评估。)


现在,考虑到所有这些,让我们看看您的程序。当Python本身运行程序时,它将完全忽略.pyi文件。当它遇到:

from library import List
def f() -> List[str]:
return List(*range(10))

它将首先评估CCD_ 15,然后将得到的对象附加到CCD_。

但是我们遇到了一个问题!您的List类型不支持__getitem__协议,因此它不知道如何处理[str]位!所以你的代码崩溃了。

修复此问题的最简单方法是。。。

  1. library.py中修复您的类,使其也扩展Generic[T](当您扩展该类时,它会执行一些元编程,以便执行List[str])
  2. 切换到使用client.py中基于注释的语法,即do:

    def f():
    # type: () -> List[str]
    ...
    

    由于注释被Python运行时真正完全忽略,所以现在不需要以任何方式更改List类——存根对mypy来说就足够了。

    (我们在这里所做的是,mypy将完全忽略library.py,只查看library.pyi——因此,它不在乎library.py是否使List类通用。)

  3. List[str]写入字符串:

    def f() -> 'List[str]':
    ...
    

    Mypy和其他符合PEP484的类型检查器允许人们将类型提示放在字符串中,作为必要时"向前声明"类型的一种方式,但我们没有理由不能将所有内容都编码为字符串(除了它看起来有点乱)。

我推荐方法1,因为方法2和3有点粗糙和脆弱。

它失败的全部原因是键入存根对运行时没有影响。

def f() -> List[str]的返回注释在运行时进行评估。这会失败,因为库的List不是从Generic[T]继承的,所以List[str]会抛出一个错误。

此外,可变参数的注释应该是每个参数的类型。也就是说,*values: Iterable[T]意味着每个项都应该是该类型的可迭代项。你可能是指*values: T

至于它为什么失败,可能只是mypy中的一个bug。请尝试为__new__创建一个typestub,因为__new__决定了对象的类型。例如:

class List(Generic[T]):
def __new__(cls, *values: T) -> List[T]: ...
def __init__(self, *values: T) -> None: ...

最新更新