2

我正在尝试将类型存根添加到具有名为 的集合类的库中,该集合类List本质上是对 builtin 的包装器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 退出。此外,python client.py失败以TypeError: 'type' object is not subscriptable.

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

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

4

2 回答 2

6

为了了解发生了什么,我认为首先查看一些背景材料会有所帮助。

在 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))

...它会首先评估List[str]然后将结果对象附加到f.__annotations__.

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

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

  1. 修复您的类,library.py使其也扩展Generic[T](当您扩展该类时,它会进行一些元编程以便List[str]正常工作)。
  2. 切换到使用基于注释的语法client.py——也就是说,执行以下操作:

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

    ...由于 Python 运行时真正完全忽略了注释,因此现在无需以List任何方式更改类 - 存根对于 mypy 来说就足够了。

    (我们在这里所做的是 mypy 将完全忽略library.py并且只会查看library.pyi- 因此,它不关心是否library.py使List类成为通用类。)

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

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

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

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

于 2017-10-28T17:07:47.453 回答
1

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

的返回注释def f() -> List[str]在运行时进行评估。这会失败,因为您的库List不继承自Generic[T],因此List[str]会引发错误。

此外,可变参数的注释应该是每个参数的类型。即,*values: Iterable[T]意味着每个项目都应该是该类型的可迭代对象。你可能的意思是*values: T

至于为什么会失败,可能只是mypy中的一个bug。尝试制作一个 typetub for__new____new__确定对象的类型。例如:

class List(Generic[T]):
    def __new__(cls, *values: T) -> List[T]: ...
    def __init__(self, *values: T) -> None: ...
于 2017-10-27T17:56:54.730 回答