Mypy:寻找一个普通函数的完美签名

Mypy: looking for the perfect signature for an average function

我正在尝试为以下函数(Python 3.6,mypy 0.521)提出完美的函数签名:

def avg(xs):
    it = iter(xs)
    try:
        s = next(it)
        i = 1
    except StopIteration:
        raise ValueError("Cannot average empty sequence")
    for x in it:
        s += x
        i += 1
    return s / i

这段代码的好处在于,它可以与 intfloatcomplex 的迭代器一起工作,并为 [=18] 的迭代器生成正确的结果=].尝试添加签名时出现问题。我试过以下方法:

def avg(xs: t.Iterable[t.Any]) -> t.Any: ...

但是现在,调用者需要转换结果。

def avg(xs: t.Iterable[T]) -> T: ...

失败,因为T不支持加法和除法。

N = TypeVar("N", int, float, complex, datetime.timedelta)
def avg(xs: t.Iterable[N]) -> N: ...

失败,因为 int / intfloat;使用 // 几乎所有其他内容都会给出错误的结果。也很糟糕,因为代码应该适用于其他类型,只要支持加法和除法。

N = TypeVar("N", float, complex, datetime.timedelta)
def avg(xs: t.Iterable[N]) -> N: ...

这几乎是完美的,但同样,如果有人后来决定向它扔四元数,mypy 会抱怨。

...然后我也在尝试使用 abctyping.overload,但那让我无处可去。

mypy --strict 下通过的最优雅的解决方案是什么?

所以,不幸的是,Python/PEP 484 中的数字系统目前有点乱。

我们在技术上有一个 "numeric tower" 应该代表 Python 中的所有 "number-like" 实体都应该遵守的一组 ABC。

此外,Python 中的许多内置类型(例如 intfloatcomplextimedelta)不t 在类型化中从这些 ABC 继承——这意味着这些 ABC 基本上是不可用的(除非您定义了显式从这些 ABC 继承的自定义类型)。

而且使问题复杂化的是 typeshed 中的 numbers module is largely dynamically typed -- 大约一年前我开始尝试修复数字模块,我的回忆是当时的 mypy 并不强大足以准确输入数字塔。

这种情况今天可能会得到解决,但这或多或少都没有实际意义,因为 mypy 最近实现了对协议的实验性支持(例如结构类型)!事实证明,这正是我们解决您的问题并最终修复数字塔所需要的(一旦将协议添加到 PEP 484 和打字模块)。

现在,您需要做的是:

  1. 安装typing_extensions模块(python3 -m pip install typing_extensions
  2. 从 Github 安装最新版本的 mypy (运行 python3 -m pip install -U git+git://github.com/python/mypy.git)

然后我们可以为 "supports add or divide" 类型定义一个协议,如下所示:

from datetime import timedelta

from typing import TypeVar, Iterable
from typing_extensions import Protocol

T = TypeVar('T')
S = TypeVar('S', covariant=True)

class SupportsAddAndDivide(Protocol[S]):
    def __add__(self: T, other: T) -> T: ...

    def __truediv__(self, other: int) -> S: ...

def avg(xs: Iterable[SupportsAddAndDivide[S]]) -> S:
    it = iter(xs)
    try:
        s = next(it)
        i = 1
    except StopIteration:
        raise ValueError("Cannot average empty sequence")
    for x in it:
        s += x
        i += 1
    return s / i

reveal_type(avg([1, 2, 3]))
reveal_type(avg([3.24, 4.22, 5.33]))
reveal_type(avg([3 + 2j, 3j]))
reveal_type(avg([timedelta(1), timedelta(2), timedelta(3)]))

运行 使用 mypy 会根据需要产生以下输出:

test.py:27: error: Revealed type is 'builtins.float*'
test.py:28: error: Revealed type is 'builtins.float*'
test.py:29: error: Revealed type is 'builtins.complex*'
test.py:30: error: Revealed type is 'datetime.timedelta*'