【问题标题】:Mypy: looking for the perfect signature for an average functionMypy:寻找平均函数的完美签名
【发布时间】:2017-09-23 21:07:08
【问题描述】:

我正在尝试为以下函数(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 下最优雅的解决方案是什么?

【问题讨论】:

  • 看起来 float/int 不对称意味着你不能真正为此创建一致的签名。它为整数和浮点数在数字意义上产生“正确的结果”,但avg([list of ints]) 产生一个浮点数,而avg([list of floats]) 也产生一个浮点数。这意味着您的函数有时会返回与给定类型相同的类型,有时还会返回另一种类型,因此它没有根据其输入类型始终可定义的返回类型。 mypy 是否允许像“数字”这样的类型(如numbers.Number)?
  • 太疯狂了,numbers.Number 没有定义__add__ 或其他标准算术运算,所以我得到Unsupported left operand type for + ("Number")Unsupported operand types for / ("Number" and "int") 等。

标签: python mypy


【解决方案1】:

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

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

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

更复杂的问题是,numbers module is largely dynamically typed in 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*'

【讨论】:

  • 它适用于 mypy 4fc4ae24,但 6c409b4e 似乎引入了一项重大更改(它无法再找到 __builtins__)。谢谢!
  • @rollcat -- 这听起来像是一个临时错误,所以希望它会在今天或明天的某个时候得到修复。
  • @Michael0x2a,感谢您的实施工作。这是一个了不起的答案!
猜你喜欢
  • 2016-09-26
  • 2011-02-08
  • 1970-01-01
  • 1970-01-01
  • 2012-10-01
  • 2022-11-09
  • 1970-01-01
  • 2019-04-13
相关资源
最近更新 更多