Pint维度约束类:运行正常但Mypy未检测类型错误的解决方法
Pint单位类:同时满足运行时维度校验与Mypy类型检查
问题背景
我在用Pint库处理单位的使用与转换,想要创建仅限制为「[time]」或「[length]」维度的类。初始方案是让Time和Length类继承自Quantity:
- 运行时能正确校验维度,非法维度会抛出
DimensionalityError; - 但Mypy无法检测出将
Length实例传入期望Time类型的函数这类错误。
后来尝试让类继承自object:
- Mypy能正确检测类型错误;
- 但执行
Length/Time这类算术运算时,Mypy会报错(尽管运行时正常,因为__new__实际返回Quantity实例)。
需要解答两个问题:
- 为何Mypy无法捕获该类型错误?
- 如何实现同时满足运行时逻辑与Mypy类型检查的方案?
1. 为什么Mypy无法捕获类型错误?
当Time和Length都继承自Pint的Quantity类时,Mypy的静态类型检查只看静态继承关系,不感知你在子类中添加的运行时维度校验逻辑。在Mypy眼里,Time和Length都是Quantity的子类,属于互相兼容的类型——任何接受Quantity或其子类的函数,Mypy都会允许传入任意一个子类的实例,哪怕它们的运行时维度完全不匹配。
简单说,Mypy不知道你在子类里做了维度限制,它只认类型的继承层级,所以不会拦截把Length传给期望Time的函数这类错误。
2. 同时满足运行时逻辑与Mypy检查的方案
这里提供两种实用的实现方式:
方案一:typing.NewType + 工厂函数(推荐)
这种方式既让Mypy把Time和Length视为完全独立的类型,又保留Pint的运行时维度校验能力:
from typing import NewType from pint import UnitRegistry, DimensionalityError ureg = UnitRegistry() Quantity = ureg.Quantity # 用NewType标记独立类型,Mypy会将它们视为完全不同的类型 Time = NewType('Time', Quantity) Length = NewType('Length', Quantity) def create_time(value: float | int, unit: str) -> Time: q = Quantity(value, unit) if q.dimensionality != '[time]': raise DimensionalityError(q, '[time]') return Time(q) def create_length(value: float | int, unit: str) -> Length: q = Quantity(value, unit) if q.dimensionality != '[length]': raise DimensionalityError(q, '[length]') return Length(q) # Mypy会严格校验类型:传入Length会报错 def process_time(t: Time) -> None: print(f"Processing time: {t}") # 测试 valid_length = create_length(10, 'meter') process_time(valid_length) # Mypy抛出类型错误,运行时如果强制传入也会触发维度校验错误
- 优点:Mypy能100%区分
Time和Length类型,工厂函数保证运行时维度正确;算术运算时,底层是Quantity实例所以运行时正常,Mypy会将运算结果视为Quantity,如果需要特定类型的结果,可以额外用cast或工厂函数转换。
方案二:自定义类+类型注解优化
如果需要保留类的构造形式,可以自定义类并为算术运算添加类型注解,解决Mypy的报错问题:
from pint import UnitRegistry, DimensionalityError from typing import cast, Self ureg = UnitRegistry() Quantity = ureg.Quantity class Time: def __new__(cls, value: float | int, unit: str) -> Self: q = Quantity(value, unit) if q.dimensionality != '[time]': raise DimensionalityError(q, '[time]') # 用cast告诉Mypy返回的是Time类型,实际运行时是Quantity实例 return cast(Self, q) class Length: def __new__(cls, value: float | int, unit: str) -> Self: q = Quantity(value, unit) if q.dimensionality != '[length]': raise DimensionalityError(q, '[length]') return cast(Self, q) # 为Length的除法运算添加类型注解,告诉Mypy返回Quantity类型 def length_truediv(self: Length, other: Time) -> Quantity: return cast(Quantity, super().__truediv__(other)) Length.__truediv__ = length_truediv # 测试 t = Time(5, 'second') l = Length(10, 'meter') speed = l / t # Mypy不再报错,运行时正常返回速度单位(米/秒)
- 优点:保留了类的直接构造形式,Mypy能区分
Time和Length;通过为算术运算方法添加类型注解,解决Mypy的运算报错问题。
内容的提问来源于stack exchange,提问作者JLDiaz
相关产品推荐
相关产品推荐

