Mypy中的计算类型

Mar*_*bst 5 python python-3.x mypy

旁白:这个问题的标题并不理想。我想做的事情可以通过计算类型来实现,也可以通过其他方式来实现。

我正在编写一些代码来验证,有时将动态类型的JSON数据转换为静态Python类型。以下是一些功能:

def from_str(x: Any) -> str:
    assert isinstance(x, str)
    return x


def from_int(x: Any) -> int:
    assert isinstance(x, int)
    return x

def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
    assert isinstance(x, list)
    return [f(y) for y in x]
Run Code Online (Sandbox Code Playgroud)

这些工作很棒。我还希望能够将它们组合以转换联合类型。理想的是这样的:

union = from_union([from_str, from_int], json)
Run Code Online (Sandbox Code Playgroud)

问题是如何键入from_union函数。我的第一种方法是:

def from_union(fs: Iterable[Callable[[Any], T]], x: Any) -> T:
    for f in fs:
        try:
            return f(x)
        except AssertionError:
            pass
    assert False
Run Code Online (Sandbox Code Playgroud)

从技术上讲这是正确的。如果我们替换Union[str,int]T上述表达式键入正确的,因为from_str,由于返回的str也返回Union[str,int](类型的任何值str是类型的值Union[str,int])。但是,mypy不想执行此替换:

test/fixtures/python/quicktype.py:59: error: Argument 1 to "from_union" has incompatible type "List[Callable[[Any], object]]"; expected "Iterable[Callable[[Any], <nothing>]]"
Run Code Online (Sandbox Code Playgroud)

它似乎是正确的,object而不是推断Union[str,int]

理想情况下,我想给的类型from_union

def from_union(fs: Iterable[Union[[Callable[[Any], S], Callable[[Any], T], ...]], x: Any) -> Union[S, T, ...]):
Run Code Online (Sandbox Code Playgroud)

Python的类型不支持该功能。另一个选择是能够指定一个函数,该函数可以fs从特定调用的实际返回类型中计算出其类型,或者以其他方式进行计算。这样有可能吗?是否有其他选择可不必诉诸于此cast

Mic*_*x2a 1

正如您所推断的,不幸的是,这不可能在 Python 的类型系统中表达。map最好的可用解决方法(与 Typeshed 用于键入、filter和 等内置函数的解决方法相同zip)是滥用重载,如下所示:

from typing import Iterable, Callable, Any, Union, TypeVar, overload, List

T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')

# Note: the two underscores tell mypy that the argument is positional-only
# and that doing things like `from_union(blob, f1=from_str)` is not legal

@overload
def from_union(x: Any, 
               __f1: Callable[[Any], T1],
               ) -> T1: ...

@overload
def from_union(x: Any, 
               __f1: Callable[[Any], T1],
               __f2: Callable[[Any], T2],
               ) -> Union[T1, T2]: ...

@overload
def from_union(x: Any, 
               __f1: Callable[[Any], T1],
               __f2: Callable[[Any], T2],
               __f3: Callable[[Any], T3],
               ) -> Union[T1, T2, T3]: ...

# The fallback: give up on the remaining callables
@overload
def from_union(x: Any, 
               __f1: Callable[[Any], T1],
               __f2: Callable[[Any], T2],
               __f3: Callable[[Any], T3],
               *fs: Callable[[Any], Any]
               ) -> Union[T1, T2, T3, Any]: ...

def from_union(x: Any, *fs: Callable[[Any], Any]) -> Any:
    for f in fs:
        try:
            return f(x)
        except AssertionError:
            pass
    assert False
Run Code Online (Sandbox Code Playgroud)

这个函数的基本作用是硬编码以支持最多三个可调用函数,如果您尝试传递更多函数,则会放弃。当然,为了支持接受更多可调用对象,请添加更多重载。

这个新函数的 API 确实略有变化:需要像这样调用:

my_union = from_union(json_blob, from_str, from_int)
Run Code Online (Sandbox Code Playgroud)

如果您想要一个与原始 API 更相似并且函数优先的 API,您需要转换x为仅关键字参数(例如from_union(*fs: Callable[[Any], Any], *, x: Any) -> Any)或将函数存储在元组中,如下所示:

@overload
def from_union(fs: Tuple[Callable[[Any], T1]], x: Any) -> T1: ...

@overload
def from_union(fs: Tuple[Callable[[Any], T1], Callable[[Any], T2]], x: Any) -> Union[T1, T2]: ...

# etc...

# The final fallback: have the tuple accept any number of callables
@overload
def from_union(fs: Tuple[Callable[[Any], Any], ...], x: Any) -> Any: ...

def from_union(fs: Tuple[Callable[[Any], Any], ...], x: Any) -> Any:
    for f in fs:
        try:
            return f(x)
        except AssertionError:
            pass
    assert False
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,如果用户传递太多参数,“后备”将会在输出中引入一些动态。如果您不喜欢这个,只需删除最后的后备即可。