You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pint维度约束类:运行正常但Mypy未检测类型错误的解决方法

Pint单位类:同时满足运行时维度校验与Mypy类型检查

问题背景

我在用Pint库处理单位的使用与转换,想要创建仅限制为「[time]」或「[length]」维度的类。初始方案是让Time和Length类继承自Quantity:

  • 运行时能正确校验维度,非法维度会抛出DimensionalityError;
  • 但Mypy无法检测出将Length实例传入期望Time类型的函数这类错误。

后来尝试让类继承自object:

  • Mypy能正确检测类型错误;
  • 但执行Length/Time这类算术运算时,Mypy会报错(尽管运行时正常,因为__new__实际返回Quantity实例)。

需要解答两个问题:

  1. 为何Mypy无法捕获该类型错误?
  2. 如何实现同时满足运行时逻辑与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 22:11:06