如何在Python中为通用数字类型添加类型提示?

55

如果这个问题以前被问过,请原谅我,但我找不到相关的答案。

考虑一个以数字类型为输入参数的函数:

def foo(a):
    return ((a+1)*2)**4;

这适用于整数、浮点数和复数。

是否有一种基本类型,以便我可以进行类型提示(使用真实存在的类型/基类),例如:

def foo(a: numeric):
    return ((a+1)*2)**4;

此外,我需要将其用于集合类型参数,例如:

from typing import Collection;
def foo(_in: Collection[numeric]):
    return ((_in[0]+_in[1])*2)**4;
3个回答

68

PEP 3141添加了数字的抽象基类,因此您可以使用:

from numbers import Number

def foo(a: Number) -> Number:
    ...

4
请注意,“Number”包括“Complex”,因此isinstance(0+1j, Number)的结果为“True”。 - Walter Tross
对不起,我阅读得太匆忙了,但像我一样的人还有很多,所以我在这里留下这个警告。 - Walter Tross
11
当我调用foo(1)时,mypy会抱怨foo的类型不兼容,期望的是“Number”类型而不是“int”类型。换句话说,这个答案对于使用mypy的用户来说无法工作。请参阅此mypy问题 - normanius
4
顺便提一句,对于 foo(np.int32(1)) 也是同样的情况。mypy会产生类似的错误提示:Argument 1 to "foo" has incompatible type "floating[_32Bit]"; expected "Number"。你有什么建议适用于mypy用户吗? - normanius

25
typing模块中没有通用的数字类型,所以你需要使用Union来创建这样的类型:
from typing import Union

numeric = Union[int, float, complex]

...

要为Numpy的数字类型集合添加支持,将np.number添加到该Union中。
numeric = Union[int, float, complex, np.number]

编辑:由于所有可能的数字都是复数,您还可以使用协议typing.SupportsComplex来对任何数字进行类型提示:

from typing import SupportsComplex

def foo(a: SupportsComplex) -> SupportsComplex:
    ...

3
请注意,这不适用于numpy的数据类型,但是被接受的答案可以。 - sturgemeister
2
@sturgemeister 我有点困惑,因为 foo(np.float32(42)) 会导致我使用的静态类型检查器 mypy 发出 Argument 1 to "foo" has incompatible type "floating[_32Bit]"; expected "Number"。你用什么类型检查器验证了你的说法? - normanius
建议使用 numeric = Union[int, float, complex, np.number],这也适用于 numpy 数据类型。 - normanius
@normanius 在基本的 Python 中,isinstance(4, Number) and isinstance(np.ones((1,))[0], Number) 返回 true,但这似乎是一个高优先级的 mypy 问题,看起来很难解决 https://github.com/python/mypy/issues/3186。 - sturgemeister
@sturgemeister 谢谢!我也注意到了这个问题。请注意,typing模块中的类型构造不能用于动态类型检查。例如isinstance(4.2, numeric)会导致TypeError。 - normanius

6

目前被广泛接受的使用 Number 的解决方案存在严重问题,因为如评论中所指出的那样,对于像 mypy 和 PyRight 这样的静态类型检查器来说,int 不是 Number。这个情况已经讨论了多年,但没有明确的解决方案

另一个可能的方法是从相关问题的详细解释中提取的:

from typing import SupportsFloat as Numeric

具有以下行为:
from decimal import Decimal
from fractions import Fraction
from typing import SupportsFloat as Numeric

import numpy as np


def f(x: Numeric) -> None:
    pass


# Accepted by mypy/Pyright:
f(123)
f(np.uintc(55))
f(Fraction(-3, 2))
f(Decimal("-3.14"))
f(np.array([1, 2, 3]))  # Should an array be numeric?

# Results in type errors:
f(complex(2, 3))
f("asdf")

这种做法相当宽容,除了complex。如果您想要包含complex,只需要执行以下操作即可。

from typing import SupportsFloat, Union

Numeric = Union[SupportsFloat, complex]

或者在Python ≥3.10风格中等效地表示:

from typing import SupportsFloat, TypeAlias

Numeric: TypeAlias = SupportsFloat | complex

或许有些不幸的是,NumPy数组在"SupportsFloat"的意义上被认为是数字,但这也体现了问题“什么是数字?”的深奥哲学性。

主要缺点:

正如@lkwbr所指出的那样,“SupportsFloat”类型只适用于识别数字,它无法支持任何像加法或比较之类的运算符。

例如,Pyright会给出以下错误:

运算符"<"不支持类型 "SupportsFloat" 和 "SupportsFloat"


这会生成 Pylance 的比较运算符 linting 错误。@blhsing的答案在这方面效果更好。 - lkwbr
1
谢谢@lkwbr,你说得完全正确,我已经相应地更新了我的答案。 - Ben Mares

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接