Python - typing - 可下标类型的联合

7
我想创建一个数组类型,它应该是可下标的,且是 `typing.List` 和 `numpy.ndarray` 类型的联合体。我知道 `numpy` 没有内置存根文件,但是这里那些 numpy 存根文件(由 Machinalis 编写)应该可以正常工作,因为它们是可下标的。
以下是期望的行为:
def foo(bar: Array[int])->None:
    pass

foo([1,2,3])          # No typing error
foo(numpy.arange(4))  # No typing error
foo((1,2,3))          # Error: Expected Array[int], got Tuple[int]
foo([1.,2.,3.])       # Error: Expected Array[int], got Array[float]

我尝试了几种方法,但都没有按预期工作。

你会如何在 Python 3.7 中做到这一点?

即使它不能满足元组错误,我也会接受某种鸭子类型的解决方案。重点是创建可下标联合的可下标类型。

谢谢。

我的最佳尝试: (mypy 错误���在注释中)

class _meta_getitem(type):
    def __getitem__(cls, x):
        return cls.__getitem__(cls, x)

class Array(metaclass=_meta_getitem):

    def __getitem__(self, element_type: type) -> type:
        array_type = typing.Union[List[element_type],  # error: Invalid type "element_type"
                                  numpy.ndarray[element_type]]
        return typing.NewType("Array[{}]".format(element_type.__name__), 
                              array_type)  # The type alias to Union is invalid in runtime context

if __name__ == "__name__":
    x: Array[int] = numpy.arange(4) # "Array" expects no type arguments, but 1 given

Union[List[int], numpy.ndarray[int]] 不起作用?你尝试了什么? - chepner
它可以工作(目前我使用的是这个)但我想要一个通用、更简洁的解决方案,这意味着我可以在某个地方定义“Array”,然后我可以使用“Array[int]”、“Array[float]”、“Array[Bar]”或其他类型,而不必为每种元素类型定义一个特殊的类型。我尝试过这样做: 使用“metaclass”,创建一个可订阅的“Array”类,其“__getitem__”方法接受一个“type”作为参数并返回“Union”。 - bonoboris
啊,好的,所以你想要“因子化”索引,例如 Union[List, ndarray][int] 而不是 Union[List[in], ndarray[int]](然后基本上将 Array 作为 Union[List, ndarray] 的别名)。 - chepner
是的,完全正确,但显然的解决方案Array = typing.Union[List, numpy.ndarray]不起作用。也就是说,x:Array[int] =[1,2]会引发“错误:类型别名的参数数量错误,期望值:0,给定值:1”。 - bonoboris
好的,抱歉,我的意思是这只是伪代码,而不是实际解决方案。 - chepner
1个回答

9
创建一个类型别名Union[List[T], Array[T]]应该可以工作:
from typing import TypeVar, Union, List

T = TypeVar('T')
Array = Union[List[T], numpy.ndarray[T]]

def foo(bar: Array[int]) -> None: pass

请查看mypy文档中关于通用类型别名的更多信息,了解有关此技术的详细信息。
由于numpy.ndarray在运行时实际上不是可订阅的,只存在于类型提示世界中,因此此代码可能在运行时失败。您可以通过将自定义类型提示隐藏在typing.TYPE_CHECKING保护后面来解决此问题,在运行时始终为false,在类型检查时间为true。
在Python 3.7+中,您可以相对清晰地完成此操作:
from __future__ import annotations
from typing import TypeVar, Union, List, TYPE_CHECKING

if TYPE_CHECKING:
    T = TypeVar('T')
    Array = Union[List[T], numpy.ndarray[T]]

def foo(bar: Array[int]) -> None: pass

对于旧版Python 3,您需要将Array[int]包装在字符串中:

from typing import TypeVar, Union, List, TYPE_CHECKING

if TYPE_CHECKING:
    T = TypeVar('T')
    Array = Union[List[T], numpy.ndarray[T]]

def foo(bar: "Array[int]") -> None: pass

请注意,试图在运行时通过组合多个其他类型提示来构建自己的Array类型提示不太可能成功:静态分析工具(如mypy)通过实际分析您的代码而不是运行它来工作:它实际上不会尝试评估自定义Array类中的任何内容。
更一般地说,在运行时“使用”类型提示往往充满风险:它们真正的意图只是作为类型提示。
最后,您似乎误解了NewType的用途。我建议阅读相关文档以获取更多信息

谢谢你的好回答,我确实不太熟悉Python的typing模块。我认为这个方法很好,我其实已经意识到我的numpy存根文件没有被mypy正确处理,而是转换成了Any。一旦我把它们配合起来并检查是否按预期工作,我会验证你的答案。 - bonoboris
对于我们这些从示例中学习的人,您能否指出一些numpy/scipy世界中好的typing示例?(对于一个新问题来说太简单了)谢谢。 - denis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接