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,感謝您的實施努力。這是一個了不起的答案! –