有没有办法使用Python类型提示作为单位?类型提示文档显示了一些示例,这些示例表明可以使用NewType
,但这些示例也表明,添加相同"新类型"的两个值不会给出"新类型"的结果,而是给出基类型的结果。有没有办法丰富类型定义,以便您可以指定像单位一样工作的类型提示(不是因为它们转换,而只是为了在获得不同的单位时收到类型警告(?允许我这样做或类似的事情:
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 中的类型提示是否能够处理其中的一些功能。
您可以通过创建一个类型存根文件来执行此操作,该文件定义了__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
的值。Seconds
和Meters
被定义为Unit
的子类,T
分别等于Seconds
和Meters
。
这样,类型检查器知道用Seconds
加/减法会获取并返回Seconds
类型的其他值,对于Meters
也是如此。
此外,我们将__mul__
和__rmul__
Unit
定义为接受类型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
文件。
@Artemis的答案非常好,但是当与MyPy一起使用时会抛出错误(@Artemis使用的是Pylance(。
我对units.pyi
进行了以下修改(基于 @Artemis 的建议(,它似乎运行良好:
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能够使用混合类型捕获操作。
答案不是在您链接的页面上吗?
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++ 中实现的全维分析。