2017-09-23 69 views
3

我试图想出以下功能完善的函数签名(Python的3.6,mypy 0.521):Mypy:寻找完美的签名,平均功能

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,float,complex,以及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下可以通过的最优雅的解决方案?

+0

好像浮/ INT不对称意味着你真的不能创建此一致的签名。它产生“正确的结果”,在数字意义上,对于整型和浮点,但'AVG([整数列表])'产生浮动,同时'AVG([浮点值列表])'还产生浮动。这意味着你的函数有时会返回它给相同的类型,有时另一种类型,所以它没有返回类型在其输入类型方面一致定义。 mypy是否允许像“number”这样的类型(如'numbers.Number')? – BrenBarn

+0

疯狂的是,'numbers.Number'没有定义'__add__'或其他标准的算术运算,所以我得到'不支持的左操作数类型+(“数字”)','不支持的操作类型/(“数字”和“INT”)'等 – rollcat

回答

1

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

我们在技术上有一个"numeric tower",它应该代表一组基于Python的所有“数字”实体都应该遵守的ABC。

此外,许多内置类型在Python(如intfloatcomplextimedelta)不从typeshed这些基本知识继承 - 这意味着这些基本知识基本上是不可用的(除非在案件您可以在其中定义从那些ABCs明确继承的自定义类型)。

为了解决这个问题,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

我们可以然后定义用于一个协议“支持添加或隔膜”类型,像这样:

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

它与mypy'4fc4ae24',但'6c409b4e'似乎已经引入了重大更改(它再也找不到'__builtins__')。谢谢! – rollcat

+0

@rollcat - 听起来就像一个临时的错误,所以希望它会被某个时候在今天或明天固定。 – Michael0x2a

+0

@ Michael0x2a,感谢您的实施努力。这是一个了不起的答案! –