4

我正在尝试为以下函数(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

这段代码的好处在于,它可以与 , , 的可迭代对象一起工作,并产生正确的结果,int但也可以为. 尝试添加签名时会出现问题。我尝试了以下方法:floatcomplexdatetime.timedelta

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; using//几乎对其他所有内容都给出了错误的结果。也很糟糕,因为代码应该适用于其他类型,只要支持加法和除法。

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

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

...然后我也尝试了一些东西,abc但这typing.overload让我无处可去。

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

4

1 回答 1

1

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

从技术上讲,我们有一个“数字塔”,它应该代表一组 ABC,Python 中的所有“类似数字”的实体都应该遵守这些 ABC。

此外, Python中的许多内置类型(例如intfloatcomplextimedelta来自那些 ABC)。

更复杂的是,numbers 模块在很大程度上是在 typeshed 中动态输入的——大约一年前,我尝试修复了 numbers 模块,我记得当时 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

然后我们可以为“支持加法或除法”类型定义一个协议,如下所示:

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*'
于 2017-09-24T22:21:28.020 回答