Python 存根文件中的Mypy泛型类

Python 存根文件中的Mypy泛型类,python,generics,stub,mypy,Python,Generics,Stub,Mypy,我在玩弄向一个库添加类型存根,这个库有一个名为List的集合类,它本质上是内置列表的包装器。出于所有实际目的,您可以假设它如下所示: # library.py class List: def __init__(self, *values): self.values = values 现在,在我的存根文件library.pyi中,我有: # library.pyi from typing import Generic, TypeVar, Iterable T = Type

我在玩弄向一个库添加类型存根,这个库有一个名为
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”对象不可下标

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


此外,是否有任何方法可以获得我想要的(即拥有
mypy client.py
fail)?

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

def()->List[str]
的返回注释在运行时进行计算。这会失败,因为库的
列表
不是从
Generic[T]
继承的,因此
List[str]
会抛出错误

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

至于它失败的原因,可能只是mypy中的一个bug。尝试为
\uuuu new\uuuu
创建类型存根,因为
\uuuu new\uuuu
决定对象的类型。例如:

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

为了理解正在发生的事情,我认为首先回顾一些背景资料会有所帮助

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

基本上,Python所做的是接受您包含的任何注释,对它们求值,然后将它们添加到函数的
\uuuuu注释\uuuu
字段中。例如,请尝试运行以下代码:

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代表范围(4)]
,然后
max(3,4)
,然后将该数据附加到
注释中。完成此操作后,Python将不再做其他事情

简而言之,这意味着

  • Python仍然必须计算每个注释,这些注释必须是有效的Python表达式
  • 但这样做之后,注释将被忽略
  • 因此,这意味着当我们使用特定的类型提示时,每个类型提示都会在函数定义期间单独计算/必须是一个有效的表达式,但随后会被Python运行时后记忽略

    (作为一个警告,这种行为在将来可能会有轻微的变化:因为我们必须计算每个注释,使用类型提示确实会带来轻微的性能损失–有一些讨论可能会更改Python,因此在将来,表达式将作为字符串存储在
    \uuuuuuuuuu注释中,而不是立即进行计算。))


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

    from library import List
    
    def f() -> List[str]:
        return List(*range(10))
    
    …它将首先计算
    List[str]
    ,然后将结果对象附加到
    f.\uuuu注释\uuuu

    但是我们遇到了一个问题!您的
    列表
    类型不支持
    \uuu getitem\uuu
    协议,因此它不知道如何处理
    [str]
    位!因此您的代码崩溃

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

  • library.py
    中修复类,这样它也可以扩展
    Generic[T]
    (扩展该类时,它会执行一些元编程,以便执行
    List[str]
    操作)
  • 切换到使用
    client.py
    中基于注释的语法,即:

    def f():
        # type: () -> List[str]
        ...
    
    …由于Python运行时完全忽略注释,因此现在不需要以任何方式更改
    List
    类–存根对于mypy来说就足够了

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

  • 将[str]
  • 列为字符串:

    def f() -> 'List[str]':
        ...
    
    Mypy和其他兼容PEP 484的类型检查器允许人们在字符串中输入类型提示,作为必要时“向前声明”类型的一种方式,但我们没有理由不能将所有内容都编码为字符串(除了它看起来有点凌乱)


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

    >失败的全部原因是因为键入存根对运行时没有影响。[…]>返回注释
    def()->List[str]
    是在运行时计算的。我想让我困惑的是存根和类型提示之间的区别。我的思维模式是,在执行代码之前,两者基本上都被剥离了。在运行时计算
    def()->List[str]
    是什么意思?