Mypy:寻找平均函数的完美签名

时间:2017-09-23 21:07:09

标签: python mypy

我试图为以下函数提供完美的函数签名(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的迭代,并为datetime.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;使用//会给几乎所有其他内容带来错误的结果。也很糟糕,因为代码应该适用于其他类型,只要支持添加和除法。

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

这几乎是完美的,但再次,如果有人后来决定扔掉四元数,mypy会抱怨。

...然后我也尝试了abctyping.overload,但这让我无处可去。

mypy --strict下会传递最优雅的解决方案是什么?

1 个答案:

答案 0 :(得分:1)

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

我们在技术上有一个"numeric tower",它应该代表一组ABC,它们应该服从Python中所有“类似数字”的实体。

此外,Python中的许多内置类型(例如intfloatcomplextimedelta)都不会从这些ABCs中继承typeshed - 这意味着这些ABC基本上不可用(除非您定义明确从这些ABC继承的自定义类型)。

为了解决这个问题,类型中的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
  3. 然后,我们可以为“支持添加或分割”类型定义协议,如下所示:

    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*'