使用 Python 类型提示对数字强制执行单位

Posted

技术标签:

【中文标题】使用 Python 类型提示对数字强制执行单位【英文标题】:Enforcing units on numbers using Python type hints 【发布时间】:2020-10-17 13:52:38 【问题描述】:

有没有办法使用 Python 类型提示作为单位? type hint docs 显示了一些示例,表明可能使用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 中的类型提示是否能够处理其中的一些功能。

【问题讨论】:

我强烈建议您尝试mypy,它会为您提供静态类型检查,这意味着它会在您运行程序之前告诉您是否有错误。它使用标准库typing。 vscode addon for mypy @ninMonkey:提问者已经在使用typing 和某种静态检查器,可能是 mypy(但也可能是 IDE 集成检查器或其他东西)。 【参考方案1】:

您可以通过创建一个类型存根文件来做到这一点,该文件定义了__add__/__radd__ 方法(定义+ 运算符)和__sub__/__rsub__ 方法(定义- 运算符)。当然,其他运算符还有很多类似的方法,但为了简洁起见,本示例仅使用这些方法。

units.py

这里我们将单位定义为int 的简单别名。这最大限度地降低了运行时成本,因为我们实际上并没有创建一个新类。

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,其中加法/减法获取并返回类型参数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 文件甚至不会被加载。

【讨论】:

优秀的答案。赞赏!【参考方案2】:

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,就不可能对as available in Ada和implementable in C++做全维度分析了。

【讨论】:

不,因为问题中已经给出了原因:Seconds(5)+ Seconds(8) 被视为常规 float,而不是 Seconds 对象。 啊,那么time1 + Meters(5) 应该但不会给出类型提示错误?

以上是关于使用 Python 类型提示对数字强制执行单位的主要内容,如果未能解决你的问题,请参考以下文章

defrecord 构造函数中未强制执行类型提示

如果返回值混合使用什么类型的提示?

对函数声明强制执行 TSX 道具类型内联,该函数声明使用对象解构来声明道具

python基础学习

python数据类型强制转换和运算符

0-python变量及基本数据类型