如何让 mypy 知道现有类型支持某些属性和方法?

How to let mypy know that an existing type supports certain properties and methods?

我正在自学 Python 并尝试通过 mypy 的类型检查系统,但我有点迷失在类型中,classes,抽象 classes 、泛型等。

所以,我想做一个generic/abstract type/class来表示日期,指定这个type/class必须有yearmonthday 属性,并且必须支持比较运算符 <<=,可能还支持其他方法。我知道这听起来确实像是抽象 class 的工作,但我不精通 OO,而且我之前尝试使用抽象 class 未能通过 mypy 的类型检查。基于 generic/abstract 模式,我定义了一个将 n 个月添加到日期的函数。接下来,我想根据模块 datetime 中的日期类型定义一个 specific/concrete 日期类型,并重新定义 add_months 函数以使用该类型进行操作,但我的想法当然是调用为 generic/abstract 模式编写的函数,而不是复制代码。

希望下面的代码能让我的意图更清楚(我的代码分为两个文件):

文件dates_generic.py:

from typing import Callable, TypeVar


Year = int

Month = int

Day = int


class A:
    year: Year
    month: Month
    day: Day

    def __lt__(self: A, other: A) -> bool:
        ...

    def __le__(self: A, other: A) -> bool:
        ...


Date = TypeVar('Date', bound=A)


def add_months(x: Date,
               n: int,
               days_in_month: Callable[[Year, Month], int],
               date_make: Callable[[Year, Month, Day], Date])-> Date:
    r = (x.month + n - 1) % 12
    q = (x.month + n - 1) // 12

    y = x.year + q

    m = r + 1

    d = min(x.day, days_in_month(y, m))

    return date_make(y, m, d)

文件dates_pylib.py:


import calendar as cal

import dates_generic as dg

import datetime as dt

from dates_generic import Year, Month, Day

from typing import NewType


DateP = NewType('DateP', dt.date)


def date_make(y: Year, m: Month, d: Day) -> DateP:
    return DateP(dt.date(y, m, d))

def days_in_month(y: Year,
                  m: Month):
    return cal.monthrange(y, m)[1]

def add_months(x: DateP,
               n: int) -> DateP:
    return dg.add_months(x, n, days_in_month, date_make)

我的问题是 mypy 仍然对我的函数提出以下问题 dates_pylib.add_months:

Value of type variable "Date" of "add_months" cannot be "DateP"

和我用作 IDE 的 PyCharm 添加了自己的:

Expected type '(int, int, int) -> Any' (matched generic type '(int, int, int) -> Date'), got '(y: int, m: int, d: int) -> DateP' instead

从第 1 条消息,我似乎明白 mypy 不知道类型 DateP 实现了年月日属性和比较运算符。如果我在 dates_generic.py 中删除 bound=A,此消息就会消失,但是 mypy 会抱怨无界类型 Date 没有 yearmonthday属性。

第二条消息对我来说意义不大,因为我从 PEP-0484 中读到“每种类型都与任何类型一致。”,所以我希望 (int, int, int) -> Any 可以替换为 (int, int, int) -> DateP.

我可能正在寻找类似于 Haskell 的类型class 约束的东西,它允许您指定类型必须支持的方法,但我不确定如何在Python.

听起来您想描述您的 Date 实现必须使用协议(昵称为“静态鸭子类型”)必须具备的功能。协议完全是另一种打字的东西,一次学习所有这些东西肯定很多,但是一旦你习惯了它们就会非常好。

定义一个协议告诉类型检查器“如果你发现任何东西做所有这些事情,它就是这个协议的一个实现者,这意味着那些期望这个协议的实现者应该对它感到满意”。它很像一个 Abstract Base Class,但也不是 - 它只是一个接口规范,实际上没有人需要从它继承(并且从协议继承基本上是一种无操作 - mypy 会弄清楚class 是否实现了协议,无论您是否声明 class 继承自该协议)。您可以阅读有关协议的 PEP here.

您可能不需要它,或者可能已经找到它,但由于听起来您正在尝试描述和重新实现 datetime 的功能子集,因此 datetime type stubs 可能是对你也有用。