简体   繁体   English

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

[英]Enforcing units on numbers using Python type hints

Is there a way to use Python type hints as units?有没有办法使用 Python 类型提示作为单位? The type hint docs show some examples that suggest it might be possible using NewType , but also those examples show that addition of two values of the same "new type" do not give a result of the "new type" but rather the base type.类型提示文档显示了一些示例,这些示例表明使用NewType是可能的,但这些示例也表明添加两个相同“新类型”的值不会给出“新类型”的结果,而是基本类型。 Is there a way to enrich the type definition so that you can specify type hints that work like units (not insofar as they convert, but just so that you get a type warning when you get a different unit)?有没有一种方法可以丰富类型定义,以便您可以指定像单位一样工作的类型提示(不是在它们转换的范围内,而是在您获得不同单位时收到类型警告)? Something that would allow me to do this or similar:可以让我这样做或类似的事情:

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.

I know runtime libraries for units exist, but my curiosity is if type hints in python are capable of handling some of that functionality.我知道存在单元的运行时库,但我的好奇心是 python 中的类型提示是否能够处理其中的某些功能。

You can do this by creating a type stub file, which defines the acceptable types for the __add__ / __radd__ methods (which define the + operator) and __sub__ / __rsub__ methods (which define the - operator).您可以通过创建一个类型存根文件来做到这一点,该文件定义了__add__ / __radd__方法(定义+运算符)和__sub__ / __rsub__方法(定义-运算符)的可接受类型。 There are many more similar methods for other operators of course, but for the sake of brevity my example only uses those.当然,其他运算符还有更多类似的方法,但为了简洁起见,我的示例仅使用这些方法。

units.py

Here we define the units as simple aliases of int .在这里,我们将单位定义为int的简单别名。 This minimises the runtime cost, since we aren't actually creating a new class.这最大限度地降低了运行时成本,因为我们实际上并没有创建新的 class。

Seconds = int
Meters = int

units.pyi

This is a type stub file .这是一个类型存根文件 It tells type checkers the types of everything defined in units.py , instead of having the types defined within the code there.它告诉类型检查器在units.py中定义的所有内容的类型,而不是在代码中定义类型。 Type checkers assume this is the source of truth, and don't raise errors when it differs from what is actually defined in 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"]): ...

Here we define Unit as a generic type inheriting from int , where adding/subtracting takes and receives values of type parameter T .在这里,我们将Unit定义为从int继承的泛型类型,其中加法/减法获取和接收类型参数T的值。 Seconds and Meters are then defined as subclasses of Unit , with T equal to Seconds and Meters respectively.然后将SecondsMeters定义为Unit的子类,其中T分别等于SecondsMeters

This way, the type checker knows that adding/subtracting with Seconds takes and receives other values of type Seconds , and similarly for Meters .这样,类型检查器就知道使用Seconds进行加法/减法会获取并接收Seconds类型的其他值,对于Meters也是如此。

Also, we define __mul__ and __rmul__ on Unit as taking a parameter of type int and returning T - so Seconds(1) * 5 should have type Seconds .此外,我们将Unit上的__mul____rmul__定义为采用int类型的参数并返回T - 所以Seconds(1) * 5应该具有Seconds类型。

main.py

This is your code.这是你的代码。

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.

Of course, all of this is just type checking.当然,所有这些都只是类型检查。 You can do what you like at run time, and the pyi file won't even be loaded.你可以在运行时做你喜欢的事, pyi文件甚至不会被加载。

The answer from @Artemis is excellent, but throws error when used with MyPy (@Artemis is using Pylance). @Artemis 的回答非常好,但在与 MyPy 一起使用时会抛出错误(@Artemis 使用的是 Pylance)。

I made the following modifications to units.pyi (based on a suggestion from @Artemis) and it appears to be working well:我对 units.pyi 进行了以下修改(根据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"]): ...

The only hold-up is that you must create values using唯一的阻碍是您必须使用

v: Seconds = Seconds(1)

rather than:而不是:

v: Seconds = 1

Other than that, MyPy is able to catch operations using mixed types.除此之外,MyPy 能够捕获使用混合类型的操作。

Isn't the answer right there on the page you linked?您链接的页面上没有答案吗?

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)` 

It looks like, since 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 andimplementable in C++ .看起来,因为我们不能将一个值(只有一个类型)传递给一个 generic ,所以不可能像 Ada 中那样进行全维度分析,并且可以在 C++ 中实现

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM