使用 Python 类型提示强制使用数字单位
Enforcing units on numbers using Python type hints
有没有办法使用 Python 类型提示作为单位? type hint docs show some examples that suggest it might be possible using 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 中的类型提示是否能够处理其中的某些功能。
答案不就在那里on the page你链接了吗?
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)`
看起来,因为 we can't pass a value, only a type, into a generic, it won't be possible to do full dimensional analysis as available in Ada and implementable in C++。
您可以通过创建类型存根文件来完成此操作,该文件定义 __add__
/__radd__
方法(定义 +
运算符)和 [=16] 可接受的类型=]/__rsub__
方法(定义 -
运算符)。当然,对于其他运算符还有很多类似的方法,但为了简洁起见,本示例仅使用这些方法。
units.py
这里我们将单位定义为 int
的简单别名。这最大限度地减少了 运行 时间成本,因为我们 实际上 创建一个新的 class.
Seconds = int
Meters = int
units.pyi
这是一个type stub file。它告诉类型检查器 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
的generic type,其中adding/subtracting接受类型参数T
的returns值. Seconds
和 Meters
然后被定义为 Unit
的子 class,其中 T
分别等于 Seconds
和 Meters
。
这样,类型检查器知道 adding/subtracting 和 Seconds
接受和 returns 类型 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
文件甚至不会被加载。
@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"]): ...
唯一的hold-up是您必须使用
创建值
v: Seconds = Seconds(1)
而不是:
v: Seconds = 1
除此之外,MyPy 能够捕获使用混合类型的操作。
有没有办法使用 Python 类型提示作为单位? type hint docs show some examples that suggest it might be possible using 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 中的类型提示是否能够处理其中的某些功能。
答案不就在那里on the page你链接了吗?
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)`
看起来,因为 we can't pass a value, only a type, into a generic, it won't be possible to do full dimensional analysis as available in Ada and implementable in C++。
您可以通过创建类型存根文件来完成此操作,该文件定义 __add__
/__radd__
方法(定义 +
运算符)和 [=16] 可接受的类型=]/__rsub__
方法(定义 -
运算符)。当然,对于其他运算符还有很多类似的方法,但为了简洁起见,本示例仅使用这些方法。
units.py
这里我们将单位定义为 int
的简单别名。这最大限度地减少了 运行 时间成本,因为我们 实际上 创建一个新的 class.
Seconds = int
Meters = int
units.pyi
这是一个type stub file。它告诉类型检查器 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
的generic type,其中adding/subtracting接受类型参数T
的returns值. Seconds
和 Meters
然后被定义为 Unit
的子 class,其中 T
分别等于 Seconds
和 Meters
。
这样,类型检查器知道 adding/subtracting 和 Seconds
接受和 returns 类型 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
文件甚至不会被加载。
@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"]): ...
唯一的hold-up是您必须使用
创建值v: Seconds = Seconds(1)
而不是:
v: Seconds = 1
除此之外,MyPy 能够捕获使用混合类型的操作。