如何让Pydantic BaseModel子类自定义__init__仍显示完整Pylance签名?
是否存在一种方法,能实现以下需求:
- 为
pydantic.BaseModel的孙类添加自定义初始化逻辑, - 让Pylance显示完整的方法签名,
- 无需手动将所有基类字段复制到每个子类的构造函数中?
简化场景
假设存在以下代码结构:
from pydantic import BaseModel class NumberWithMetadata(BaseModel): value: float | int source: str class FloatWM(NumberWithMetadata): value: float
此时一切正常,在VS Code中输入FloatWM(时,Pylance的智能提示会显示完整且正确的签名:(*, value: float, source: str) -> FloatWM,其中value被限制为float类型。
但如果为子类添加自定义初始化逻辑,用**kwargs避免依赖基类字段细节:
class FloatWM(NumberWithMetadata): value: float def __init__(self, value: float, **kwargs): super().__init__(value = value * 1.618, **kwargs)
代码运行正常,但此时Pylance仅显示签名:(*, value: float, **kwargs: Unknown) -> FloatWM,丢失了source等基类字段的提示。
请问有没有语法或模式能让VS Code中的子类获得清晰的智能提示?
实际场景
为避免XY问题,说明真实业务场景:需要构建多个包含不同字段的类,部分字段包装pint.Quantity,每个字段指定所需维度和基础单位,构造时会强制校验维度,未指定单位时应用默认单位。
from typing import Any, Annotated from pydantic import BaseModel import pint from pydantic_pint import PydanticPintQuantity class ValueWithAttribution(BaseModel): value: Any source: str # 更多字段... class NumberWithAttribution(ValueWithAttribution): value: float | int QuantityOrLiteral = float | int | str | PydanticPintQuantity class QuantityWithAttribution(ValueWithAttribution): value: pint.Quantity def __init__(self, value: QuantityOrLiteral, **kwargs): units = self.__class__.__annotations__.get("value").__metadata__[0].units if isinstance(value, (float, int)): super().__init__(value=pint.Quantity(value, units), **kwargs) else: if isinstance(value, str): value = pint.Quantity(value) if value.units.is_compatible_with(units): super().__init__(value=value, **kwargs) else: # 抛出异常 class Length(QuantityWithAttribution): value: Annotated[pint.Quantity, PydanticPintQuantity("meters")] class Weight(QuantityWithAttribution): value: Annotated[pint.Quantity, PydanticPintQuantity("kg")] class Person(BaseModel): height: Length weight: Weight me = Person( height = Length('70 inches', source='我自己说的'), weight = Weight(84, source='我那不靠谱的秤') )
题外话(非本次问题范围):若要让Pylance接受Length()和Weight()构造时的位置参数,似乎必须在每个叶子类中重复定义构造函数,尽管父类已有该签名:
class Length(QuantityWithAttribution): value: Annotated[pint.Quantity, PydanticPintQuantity("meters")] # 无功能需求,但为让Pylance接受位置参数必须添加 def __init__(self, value: QuantityOrLiteral, **kwargs): super().__init__(value=value, **kwargs)
方法1:使用Pydantic v2的model_validator替代自定义__init__
Pydantic v2推荐用model_validator处理实例化时的自定义逻辑,而非重写__init__。这种方式既能保留Pylance的完整签名提示,又能实现自定义校验/转换逻辑:
针对简化场景的改造:
from pydantic import BaseModel, model_validator class NumberWithMetadata(BaseModel): value: float | int source: str class FloatWM(NumberWithMetadata): value: float @model_validator(mode='before') def transform_value(cls, values): # 对传入的参数字典做修改 if 'value' in values: values['value'] = values['value'] * 1.618 return values
此时Pylance仍会显示完整签名(*, value: float, source: str) -> FloatWM,同时自定义逻辑生效。
针对实际场景的改造:
from typing import Any, Annotated from pydantic import BaseModel, model_validator import pint from pydantic_pint import PydanticPintQuantity class ValueWithAttribution(BaseModel): value: Any source: str # 更多字段... class QuantityWithAttribution(ValueWithAttribution): value: pint.Quantity @model_validator(mode='before') def process_quantity(cls, values): if 'value' not in values: return values value = values['value'] # 获取当前类指定的单位 units = cls.__annotations__['value'].__metadata__[0].units if isinstance(value, (float, int)): values['value'] = pint.Quantity(value, units) elif isinstance(value, str): value = pint.Quantity(value) if not value.units.is_compatible_with(units): raise ValueError(f"单位不兼容,需为{units}维度") values['value'] = value else: if not value.units.is_compatible_with(units): raise ValueError(f"单位不兼容,需为{units}维度") return values class Length(QuantityWithAttribution): value: Annotated[pint.Quantity, PydanticPintQuantity("meters")] class Weight(QuantityWithAttribution): value: Annotated[pint.Quantity, PydanticPintQuantity("kg")]
这种方式下,Length和Weight的构造函数签名会被Pylance正确识别,包含value和所有基类字段(如source)的提示,无需重复定义__init__。
方法2:为__init__添加显式类型提示(兼容Pydantic v1/v2)
如果必须保留自定义__init__,可以通过显式标注基类字段的类型让Pylance识别完整签名,虽然需要手动引用基类字段,但可以通过继承减少重复:
from pydantic import BaseModel class NumberWithMetadata(BaseModel): value: float | int source: str class FloatWM(NumberWithMetadata): value: float def __init__(self, value: float, source: str, **kwargs): super().__init__(value=value*1.618, source=source, **kwargs)
但这种方式不如model_validator优雅,仅推荐在无法升级到Pydantic v2时使用。
关于位置参数的题外话
在Pydantic v2的model_validator模式下,默认支持按字段顺序传位置参数,无需重复定义__init__。若仍有问题,可以在模型类上添加@model_config({"extra": "forbid"}),让Pylance更准确地识别参数。
内容的提问来源于stack exchange,提问作者Phrogz

