我使用pint
来使用和转换单位。我想创建将数量限制为"0"的类;[时间]";或";[长度]";尺寸,所以作为第一种方法,我做了以下操作:
from pint import Quantity, DimensionalityError
class Time(Quantity):
def __new__(cls, v: str | Quantity) -> Quantity:
obj = Quantity(v)
if not obj.check("[time]"):
raise DimensionalityError(v, "[time]")
return obj
class Length(Quantity):
def __new__(cls, v: str | Quantity) -> Quantity:
obj = Quantity(v)
if not obj.check("[length]"):
raise DimensionalityError(v, "[length]")
return obj
在运行时,它按预期工作,即:我可以执行以下操作:
1hour = Time("1h") # Works ok, variable 1hour contains `<Quantity(1, 'hour')>`
bad = Time("1meter") # As expected, raises pint.errors.DimensionalityError: Cannot convert from '1meter' to '[time]'
1meter = Length("1meter") # Ok
bad_again = Length("1h") # Ok, raises DimensionalityError
然而,从打字的角度来看,有些地方出了问题:
def myfunc(t: Time) -> str:
return f"The duration is {t}"
print(myfunc(Time("1h"))) # Ok
print(myfunc(Length("1m"))) # Type error?
对myfunc()
的第二个调用是一个类型错误,因为我传递的是Length
而不是Time
。然而,mypy
对代码很满意。所以我有一些问题:
- 为什么mypy没有发现错误
- 如何正确操作
对于1。我猜想在pint的Quantity
实现中发生了一些可疑的事情。我试过了:
foo = Quantity("3 pounds")
reveal_type(foo)
所揭示的类型是CCD_ 7而不是非常可疑的CCD_。
因此,我尝试从Time
和Length
类中删除基类Quantity
(即:它们现在从object
而不是Quantity
派生),在这种情况下,mypy
正确地管理了键入错误。
但当我尝试类似Length("60km")/Time("1h")
的东西时,它又失败了。mypy
抱怨Length
对象没有实现执行该除法所需的方法(尽管代码在运行时工作正常,因为毕竟Length
和Time
__new__()
方法返回了一个Quantity
对象,确实实现了算术运算)。
那么,再一次,有什么变通方法可以让这个想法在运行时和mypy
中都有效吗?
TL;DR:Pint有一个包装器函数可以为您做到这一点:@ureg.check('[time]')
,它位于函数定义的正上方。
讨论
一般来说,python不强制执行键入提示(请参阅此处的官方文档)。如果您想强制执行这样的行为,最好的方法就是使用isinstance()
本机方法(请在此处查找详细信息)。但是,在您的示例中,您甚至没有创建一个新类型。Time
和Length
类都将返回一个pint.util.Quantity对象。因此,以下示例将始终引发错误,即使对于Time
定义的对象也是如此:
def myfunc(t: Time) -> str: # does not work!
if isinstance(t, Time):
return f"The duration is {t}"
else:
raise DimensionalityError(t, "[time]")
解决方案
一个简单的建议是定义一个检查数量类型的函数:
def qtype_check(obj: Quantity, q_type: str): qtype_str = f"[{q_type}]" if not obj.check(qtype_str): raise DimensionalityError(obj, qtype_str)
然后简单地用你选择的方法称之为:
def myfunc(t: Quantity) -> str: qtype_check(t, 'time') return f"The duration is {t}"
如果您需要保留类定义,一个更优雅的解决方案是将数量类额外定义为例如
MyQuantity
,并在每个类型定义中更改类属性_qtype
。这样,就可以避免多次编写相同的错误代码片段。此外,您可以将qtype_check合并到类中,并通过您选择的类型类(例如Time.qtype_check(value)
)调用它,从而避免每次都要记住如何定义类类型字符串的麻烦。以下是定义:from pint import Quantity, DimensionalityError class MyQuantity(Quantity): _qtype: str = "[]" def __new__(cls, v: str | Quantity) -> Quantity: obj = Quantity(v) cls.qtype_check(obj) return obj @classmethod def qtype_check(cls, obj: Quantity): if not obj.check(cls._qtype): raise DimensionalityError(obj, cls._qtype) class Time(MyQuantity): _qtype = "[time]" class Length(MyQuantity): _qtype = "[length]" def myfunc(t: Quantity) -> str: Time.qtype_check(t) return f"The duration is {t}"
最棒的是简单地使用pint提供的包装器功能,它可以自动为您检查输入单位!这是文件。在你的情况下,它看起来像:
@ureg.check('[time]') def myfunc(t: Quantity) -> str: return f"The duration is {t}"