使用 Python 类型提示强制数字单位。

14
有没有办法将Python类型提示用作单位?类型提示文档中显示了一些示例,表明可能可以使用NewType,但是这些示例也表明,相同“new type”的两个值的加法不会得到“new type”的结果,而是基本类型。有没有一种方法来丰富类型定义,以便您可以指定像单位一样工作的类型提示(不是为了转换,而仅仅是当您获取不同单位时得到类型警告)?是否有类似这样的东西允许我执行此操作或类似操作:
Seconds = UnitType('Seconds', float)
Meters = UnitType('Meters', float)

time1 = Seconds(5)+ Seconds(8) # gives a value of type `Seconds`
bad_units1 = Seconds(1) + Meters(5) # gives a type hint error, but probably works at runtime
time2 = Seconds(1)*5 # equivalent to `Seconds(1*5)` 
# Multiplying units together of course get tricky, so I'm not concerned about that now.

我知道有针对单元的运行时库存在,但我的好奇心是想知道Python中的类型提示是否能够处理部分该功能。


我强烈推荐尝试使用 mypy,它提供了静态类型检查功能,这意味着它会在你运行程序之前告诉你是否存在错误。它使用标准库 typingVSCode 的 mypy 插件 - ninMonkey
@ninMonkey:提问者已经在使用typing和某种静态检查器,很可能是mypy(但也可能是IDE集成的检查器或其他工具)。 - user2357112
3个回答

5
你可以通过创建类型存根文件来实现此操作,该文件定义了__add__/__radd__方法(定义+运算符)和__sub__/__rsub__方法(定义-运算符)的可接受类型。当然,对于其他运算符也有许多类似的方法,但为了简洁起见,本例仅使用这些方法。

units.py

在这里,我们将单位定义为int的简单别名。这样可以最大程度地减少运行时成本,因为我们实际上并没有创建一个新类。
Seconds = int
Meters = int

units.pyi

这是一个类型存根文件。它告诉类型检查器在units.py中定义的所有内容的类型,而不是在那里定义类型。类型检查器假定这是真相来源,并且当它与实际在units.py中定义的不同时不会引发错误。
from typing import Generic, TypeVar

T = TypeVar("T")

class Unit(int, Generic[T]):
    def __add__(self, other: T) -> T: ...
    def __radd__(self, other: T) -> T: ...
    def __sub__(self, other: T) -> T: ...
    def __rsub__(self, other: T) -> T: ...
    def __mul__(self, other: int) -> T: ...
    def __rmul__(self, other: int) -> T: ...

class Seconds(Unit["Seconds"]): ...

class Meters(Unit["Meters"]): ...

在这里,我们将Unit定义为一个泛型类型,继承自int,其中添加/减去采用并返回参数类型T的值。然后,SecondsMeters被定义为Unit的子类,其中T分别等于SecondsMeters
这样,类型检查器就知道使用Seconds进行添加/减去,采用并返回其他类型Seconds的值,对于Meters也是如此。
此外,我们在Unit上定义__mul____rmul__,以带有int类型的参数并返回T - 因此,Seconds(1) * 5应该具有类型Seconds

main.py

这是你的代码。

from units import Seconds, Meters

time1 = Seconds(5) + Seconds(8)
# time1 has type Seconds, yay!

bad_units1 = Seconds(1) + Meters(5)
# I get a type checking error:
# Operator "+" not supported for types "Meters" and "Seconds"
# Yay!

time2 = Seconds(1) * 5
# time2 has type Seconds, yay!

meter_seconds = Seconds(1) * Meters(5)
# This is valid because `Meters` is a subclass of `int` (as far
# as the type checker is concerned). meter_seconds ends up being
# type Seconds though - as you say, multiplying gets tricky.

当然,所有这些只是类型检查。您可以在运行时执行任何操作,并且pyi文件甚至不会被加载。

1
非常好的回答。感谢! - Garrett Motzner

3

@Artemis的答案非常好,但在使用MyPy时会出错(@Artemis正在使用Pylance)。

我根据@Artemis的建议对units.pyi进行了以下修改,看起来效果不错:

from typing import Generic, TypeVar, Union

T = TypeVar("T")

class Unit(Generic[T]):
    def __add__(self, other: Union[T, int]) -> T: ...
    def __radd__(self, other: Union[T, int]) -> T: ...
    def __sub__(self, other: Union[T, int]) -> T: ...
    def __rsub__(self, other: Union[T, int]) -> T: ...
    def __mul__(self, other: Union[T, int]) -> T: ...
    def __rmul__(self, other: Union[T, int]) -> T: ...

    def __init__(self, val: int) -> None: ...

class Seconds(Unit["Seconds"]): ...

class Meters(Unit["Meters"]): ...


唯一的障碍是必须使用特定的方式创建数值:
v: Seconds = Seconds(1)

更好的选择是:

v: Seconds = 1

除此之外,MyPy能够捕捉使用混合类型的操作。

-1

你链接的页面上不是已经有答案了吗?

from typing import NewType

Seconds = NewType('Seconds', float)
Meters = NewType('Meters', float)

time1 = Seconds(5)+ Seconds(8) # gives a value of type `Seconds`
bad_units1 = Seconds(1) + Meters(5) # gives a type hint error, but probably works at runtime
time2 = Seconds(1)*5 # equivalent to `Seconds(1*5)` 

看起来,由于我们只能将类型而非值传递到泛型中, 所以不可能像Ada中那样进行完整的维度分析并在C++中实现


不行,因为问题中已经给出了原因:Seconds(5)+ Seconds(8) 被视为普通的 float,而不是一个 Seconds 对象。 - user2357112
啊,所以执行 time1 + Meters(5) 应该但是没有给出类型提示错误? - TamaMcGlinn

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