如何让IDE识别Python中静态声明、动态创建类的自定义类型名称?
问题背景
我正在开发一个支持自定义位宽整数类型的库,比如UInt5、Int33这类。举个例子,创建UInt12类型的代码大概是这样的:
def makeUInt(size:int) -> type[UInt]: class NewUInt(UInt): # 这里处理和size相关的逻辑 NewUInt.__name__ = f"UInt{size}" return NewUInt UInt12 = makeUInt(12) an_example_number = UInt12(508)
但我的IDE(VS Code)的IntelliSense把an_example_number的类型识别成了UInt,而不是我期望的UInt12。
关键矛盾
我本来没指望动态声明的类型能被类型提示完全识别,但我明明把UInt12作为类型别名用了。而且如果我改成继承动态创建的类的方式:
def makeUInt(size:int) -> type[UInt]: class NewUInt(UInt): # 这里处理和size相关的逻辑 NewUInt.__name__ = f"UInt{size}" return NewUInt class UInt12(makeUInt(12)): pass an_example_number = UInt12(508)
IDE就能正常识别an_example_number是UInt12类型,这说明动态创建的类其实是可以被IDE识别的,只是需要一些调整。我甚至想过让UInt维护一个已创建类的注册表,避免UInt12真的去继承,但这显然不是理想的解决方案。
需求说明
我希望(最好在Python 3.8环境下)既能保留动态创建类型的便利性,又能让IDE识别这些静态声明的自定义类型名称。我的实际场景是:库中要提供UInt16、UInt32这类常用类型,让它们能被类型提示识别;而像UInt13这类不常用的类型,由用户按需创建,不要求一定有提示。
当前现状
用最直接的动态创建赋值方式:
def makeUInt(size:int) -> type[UInt]: class NewUInt(UInt): # 这里处理和size相关的逻辑 NewUInt.__name__ = f"UInt{size}" return NewUInt UInt12 = makeUInt(12) an_example_number = UInt12(508)
IDE会把an_example_number识别为UInt,而不是期望的UInt12。
给你几个可行的解决思路,亲测有效
1. 泛型+自定义元数据(Python3.8直接能用)
我之前做类似的自定义类型库时,用泛型的思路解决过这个问题。核心是把位宽作为泛型参数,让类型检查器能区分不同的UInt类型:
首先把基类改成泛型类,然后在动态创建子类的时候,给它加上泛型参数的标记:
from typing import Generic, TypeVar, Type # 定义一个绑定到int的类型变量,用来表示位宽 SizeType = TypeVar('SizeType', bound=int) class UInt(Generic[SizeType]): def __init__(self, value: int): self.value = value def makeUInt(size: int) -> Type[UInt[SizeType]]: class NewUInt(UInt[SizeType]): # 这里写你的size相关逻辑 pass NewUInt.__name__ = f"UInt{size}" # 关键:手动设置__orig_bases__,让类型检查器识别这个类对应的泛型参数 NewUInt.__orig_bases__ = (UInt[size],) return NewUInt # 声明常用类型 UInt12 = makeUInt(12) UInt16 = makeUInt(16) an_example_number = UInt12(508)
现在IDE会把an_example_number识别成UInt[12],虽然不是完全的UInt12,但已经能明确区分不同位宽的类型,而且完全兼容Python3.8。
2. 给常用类型写存根文件(最完美的IDE识别方案)
如果你的库只需要让常用类型(比如UInt12、UInt32)被IDE识别,那直接写存根文件是最省心的。存根文件(.pyi)是给类型检查器和IDE看的,不会影响运行时逻辑:
比如你的库叫custom_uints,就创建一个custom_uints.pyi文件:
# custom_uints.pyi class UInt: def __init__(self, value: int) -> None: ... class UInt12(UInt): def __init__(self, value: int) -> None: ... class UInt16(UInt): def __init__(self, value: int) -> None: ... # 其他常用类型同理
然后你的运行时代码还是用原来的动态创建方式,IDE会优先读取存根文件的类型信息,完美识别UInt12这类类型,用户用的时候也能得到准确的提示。
3. 用TypeAlias标记(需要typing_extensions,Python3.8兼容)
如果你不想改太多代码,可以用typing_extensions里的TypeAlias来明确标记这些静态声明的类型是别名,帮助IDE识别:
from typing_extensions import TypeAlias # 保持原来的makeUInt函数不变 UInt12: TypeAlias = makeUInt(12) an_example_number = UInt12(508)
不过这个方法单独用的话,IDE可能还是会显示底层的UInt类型,最好和泛型方案结合起来用,效果会更好。
4. 模拟泛型类的行为
你还可以给动态创建的类添加一些类型检查器能识别的方法,比如__class_getitem__,让它表现得更像一个泛型类:
def makeUInt(size: int) -> type[UInt]: class NewUInt(UInt): _bit_size = size def __init__(self, value: int): super().__init__(value) NewUInt.__name__ = f"UInt{size}" # 添加类方法,让类型检查器能通过下标识别类型 @classmethod def __class_getitem__(cls, item): if item == size: return cls return makeUInt(item) NewUInt.__class_getitem__ = __class_getitem__ return NewUInt
这样IDE在识别的时候,能通过_bit_size或者__class_getitem__来区分不同的UInt子类,也能达到类似的效果。
备注:内容来源于stack exchange,提问作者D. Estes McKnight

