我们已开始在分布式 MLops 平台中添加对 pydantic v2 的支持。我们目前面临的一个核心挑战是,pydantic 实际上是我们平台中每个 python 包的一部分,并用于各种用例:
我们的核心问题是,我们需要在未知的时间内支持这两个版本。这主要是因为有些 ML 模型已经使用 pydantic v1 进行了训练,需要使用 v1 进行服务,而可能存在使用 v2 进行训练的新 ML 模型,也需要使用 v2 进行服务。在运行时,尤其是在我们的 ML 端点中,我们可能安装了 v1 或 v2,并且我们需要能够支持这两者。
这使我们陷入这样的境地:我们平台的某些组件需要在未知的时间内(可能是几周、几个月……)与两个版本兼容。这就需要提供一个跨版本兼容层,使转换更容易。
虽然我们已经能够获得最常用的 pydantic 功能测试和类型安全交叉版本(使用某种包装器/适配器代码),但 pydantic 模型配置仍然困扰着我们。这是因为可能有成百上千的地方,基本模型以不同的配置设置(可变或不可变、有或没有别名、有额外或没有等)保存在代码中。
为了缓解这种过渡,我们考虑创建跨版本兼容的 mixin 类,可以请求这些类来创建配置的 pydantic 模型。这有点像这个想法:
# type checking this code does work via something like `python -m mypy foo.py --always-true PYDANTIC_V1`
import packaging.version
import pydantic
PYDANTIC_V1 = packaging.version.parse(pydantic.__version__).major == 1
if PYDANTIC_V1:
class Immutable(pydantic.BaseModel):
"""mixin to make a pydantic model immutable"""
class Config:
allow_mutation = False
else:
from pydantic import ConfigDict
class Immutable(pydantic.BaseModel):
"""mixin to make a pydantic model immutable"""
model_config = ConfigDict(frozen=True)
为了使模型不可变,我们的客户可以简单地以这种方式声明他们的模型:
from foo import Immutable
class JobConfiguration(Immutable):
foo: str
但是,对于多重继承,这会变得很棘手。我们在 v1 中尝试了两种可能的方法,但都无法通过 pydantic mypy 插件进行类型检查
from typing import Any
from pydantic import BaseModel, Field
class Immutable(BaseModel):
"""mixin to make a pydantic model immutable"""
class Config:
allow_mutation = False
class WithAliases(BaseModel):
"""mixin to make a pydantic model to accept and serialize by alias"""
class Config:
allow_population_by_field_name = True
def json(self, by_alias: bool = True, **kwargs: Any) -> str:
return super().json(by_alias=by_alias, **kwargs)
class Customer(Immutable, WithAliases):
id_: str = Field(..., alias="id")
instance = Customer(id_="foo")
instance.id_ = "1"
在此代码上运行 mypy 失败
python -m mypy v1.py --always-true PYDANTIC_V1
v1.py:23: error: Definition of "Config" in base class "Immutable" is incompatible with definition in base class "WithAliases" [misc]
v1.py:28: error: Property "id_" defined in "Customer" is read-only [misc]
Found 2 errors in 1 file (checked 1 source file)
from typing import Any
from pydantic import BaseModel, Field
class Immutable(BaseModel, allow_mutation=False):
"""mixin to make a pydantic model immutable"""
class WithAliases(BaseModel, allow_population_by_field_name=True):
"""mixin to make a pydantic model to accept and serialize by alias"""
def json(self, by_alias: bool = True, **kwargs: Any) -> str:
return super().json(by_alias=by_alias, **kwargs)
class Customer(Immutable, WithAliases):
id_: str = Field(..., alias="id")
instance = Customer(id_="foo")
instance.id_ = "1"
在此代码上运行 mypy 也失败,尽管有不同的错误消息
python -m mypy v2.py --always-true PYDANTIC_V1
v2.py:21: error: Unexpected keyword argument "id_" for "Customer"; did you mean "id"? [call-arg]
Found 1 error in 1 file (checked 1 source file)
有谁知道在 pydantic v1 中使用多个配置混合类并让它们通过 mypy 类型检查的方法吗?
Virtualenv
Python: 3.10.14
Implementation: CPython
System
Platform: darwin
OS: posix
Dependencies
mypy 1.9.0
mypy-extensions 1.0.0
packaging 24.0
pydantic 1.10.10
tomli 2.0.1
typing-extensions 4.11.0
[tool.mypy]
plugins = [
"pydantic.mypy"
]
strict = true
implicit_reexport = true
ignore_missing_imports = true
check_untyped_defs = true
show_error_codes = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = true
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true
要处理 Pydantic v1 和 v2,请考虑为共享设置创建抽象基配置类,同时利用基于运行时检测到的 Pydantic 版本的条件导入。这样,您可以根据安装的版本动态调整使用哪些 Pydantic 特性或功能。使用环境变量或配置文件来指定系统不同部分所需的 Pydantic 版本。这种方法可以实现灵活性并保持跨平台的兼容性,而无需过早地提交单个版本。此外,彻底的测试对于确保跨版本的兼容性和功能至关重要。