是否可以在 Pydantic 模型的 `schema_extra` 配置设置上使用继承?

问题描述 投票:0回答:2

例如,我有以下

Parent
模型的玩具示例:

from pydantic import BaseModel, Extra


class Parent(BaseModel):
    class Config:
        extra = Extra.ignore
        validate_assignment = True
        schema_extra = {
            "version": "00.00.00",
            "info": "Parent description",
            "name": "Parent Name",
        }

这里的目标是所有子模型都继承相同的模式,并且可能会向该模式添加其他内容。

class Child(Parent):
    class Config:
        extra = Extra.ignore
        validate_assignment = True

        @staticmethod
        def schema_extra(
            schema: dict[str, object], model: type["Child"]
        ) -> None:
            schema['info'] = 'Child Description'
            schema['name'] = 'Child Name'
            schema['additional stuff'] = 'Something else'

上面的代码不起作用,因为子类中的

Config
完全覆盖了从父类继承的
Config
,因此在子类的模式中我们缺少
version

如前所述,我希望所有继承的类都具有相同的基本架构布局和一些元信息,并可能更改值或添加到其中。这可能吗?

编辑 @Daniil-Fajnberg 的解决方案效果很好,但有一个警告。例如:

class Child(Parent):
    class Config:
        @staticmethod
        def schema_extra(schema: dict[str, object]) -> None:
            schema["info"] = "Child Description"
            schema["additional stuff"] = "Something else"
            schema["name"] = f"{schema.get('title')}v{schema.get('version')}"

模式中

name
的结果条目将是例如:

'名称':“Nonev00.00.00”

仅供参考:在我的设置中,我也使用

schema_extra
作为父级的静态方法

python inheritance pydantic
2个回答
1
投票

更正

首先,这个说法并不完全正确:

子类中的

Config
完全覆盖了从父类继承的
Config

Config
本身是继承的。但个别
Config
属性 被覆盖

示例:

from pydantic import BaseModel, Extra


class Parent(BaseModel):
    class Config:
        extra = Extra.allow
        validate_assignment = True


class Child(Parent):
    class Config:
        extra = Extra.forbid


print(Child.__config__.extra)                # Extra.forbid
print(Child.__config__.validate_assignment)  # True

话虽如此,您面临的问题是您正在覆盖

schema_extra
中的
Child.Config
属性。换句话说
Child.Config.schema_extra
确实取代了
Parent.Config.schema_extra

如果我们希望通过子模型使配置的这个特定属性可扩展而不是让它们覆盖它,我们必须有点创意。


Pydantic 2.x 更新

使用较新的 Pydantic 版本,这变得更加容易。在基类中,无需设置额外的模式,只需使用

[__get_pydantic_json_schema__][2]
即可获取当前模式并对其进行修改。

用法示例:

from pydantic import BaseModel, GetJsonSchemaHandler, Field
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema

def schema_extra(schema: dict[str, Any], model: type["Parent"]) -> None:
    schema["version"] = "1"
    schema["info"] = "Parent description"
    
class Parent(BaseModel):
    some_parent_property: str 

class Child(Parent):
    @classmethod
    def __get_pydantic_json_schema__(
        cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema ["info"] = "Child Description"
        json_schema ["additional stuff"] = "Something else"
    return json_schema


print(Child.schema_json(indent=4))

更新解决方案

我建议定义一个自定义

BaseModel
以在整个应用程序中使用,挂钩其
__init_subclass__
方法并应用自定义函数来“继承”所有祖先模型的
schema_extra
配置属性(相反 MRO)迭代。

为了确保任何

schema_extra
function 调用都被推迟到实际构造模式字典之前,我们需要确保每个子类都将其
schema_extra
配置值由用户定义保存在单独的配置中属性。这与我最初的解决方案不同(见下文),这是次优的,因为它在类创建期间立即调用函数,因此无法访问稍后创建的某些模式属性/键。

这似乎有效:

from collections.abc import Iterable
from functools import partial
from inspect import signature
from typing import Any, ClassVar, Union

from pydantic import BaseModel as PydanticBaseModel
from pydantic.config import BaseConfig as PydanticBaseConfig, SchemaExtraCallable


class BaseConfig(PydanticBaseConfig):
    own_schema_extra: Union[dict[str, Any], SchemaExtraCallable] = {}


class BaseModel(PydanticBaseModel):
    __config__: ClassVar[type[BaseConfig]] = BaseConfig

    @classmethod
    def __init_subclass__(cls, **kwargs: object) -> None:
        cls.__config__.own_schema_extra = cls.__config__.schema_extra
        cls.__config__.schema_extra = partial(
            inherited_schema_extra,
            base_classes=(
                base for base in reversed(cls.__mro__)
                if issubclass(base, BaseModel)
            ),
        )


def inherited_schema_extra(
    schema: dict[str, Any],
    model: type[BaseModel],
    *,
    base_classes: Iterable[type[BaseModel]],
) -> None:
    for base in base_classes:
        base_schema_extra = base.__config__.own_schema_extra
        if callable(base_schema_extra):
            if len(signature(base_schema_extra).parameters) == 1:
                base_schema_extra(schema)
            else:
                base_schema_extra(schema, model)
        else:
            schema.update(base_schema_extra)

请注意,所有使用

BaseConfig
注释的业务主要只是为了提高类型安全性并在使用新的
own_schema_extra
配置属性之前正确定义它。这样做并不是绝对必要的,但这使得这是一个更干净的解决方案。

用途:

class Parent1(BaseModel):
    class Config:
        schema_extra = {
            "version": "1",
            "info": "Parent1 description",
        }


class Parent2(BaseModel):
    class Config:
        schema_extra = {
            "version": "2",
            "info": "Parent2 description",
        }


class Child(Parent2, Parent1):
    class Config:
        @staticmethod
        def schema_extra(schema: dict[str, Any]) -> None:
            schema["info"] = "Child Description"
            schema["additional stuff"] = "Something else"
            schema["title+version"] = f'{schema["title"]}v{schema["version"]}'


print(Child.schema_json(indent=4))

输出:

{
    "title": "Child",
    "type": "object",
    "properties": {},
    "version": "2",
    "info": "Child Description",
    "additional stuff": "Something else",
    "title+version": "Childv2"
}

正如您在这里所看到的,模式的多重继承得到了正确的支持(因为我们遵循 MRO 来构造它),并且对

schema_extra
函数的延迟调用允许我们以以下方式之一访问模式的
title
键:他们构建
title+version
值。


初始解决方案(请参阅OP/注释以了解警告)

我建议挂钩

__init_subclass__
并应用自定义函数来迭代地“继承”所有父模型的
schema_extra
配置属性(相反 MRO):

from __future__ import annotations
from inspect import signature

from pydantic import BaseModel as PydanticBaseModel


def inherit_model_schema_extra(model: type[BaseModel]) -> None:
    schema_extra: dict[str, object] = {}
    for parent in reversed(model.__mro__):
        if not issubclass(parent, BaseModel):
            continue
        parent_schema_extra = parent.__config__.schema_extra
        if callable(parent_schema_extra):
            if len(signature(parent_schema_extra).parameters) == 1:
                parent_schema_extra(schema_extra)
            else:
                parent_schema_extra(schema_extra, model)
        else:
            schema_extra.update(parent_schema_extra)
    model.__config__.schema_extra = schema_extra


class BaseModel(PydanticBaseModel):
    @classmethod
    def __init_subclass__(cls, **kwargs: object) -> None:
        inherit_model_schema_extra(cls)

此处处理

schema_extra
的方式(即作为可调用或作为字典)本质上是从当前稳定版本中的
model_process_schema
函数的处理方式复制的。 (参见第 591 行及后续)

用途:

class Parent(BaseModel):
    class Config:
        schema_extra = {
            "version": "00.00.00",
            "info": "Parent description",
        }


class Child(Parent):
    class Config:
        @staticmethod
        def schema_extra(schema: dict[str, object]) -> None:
            schema["info"] = "Child Description"
            schema["additional stuff"] = "Something else"


print(Child.schema_json(indent=4))

输出:

{
    "title": "Child",
    "type": "object",
    "properties": {},
    "version": "00.00.00",
    "info": "Child Description",
    "additional stuff": "Something else"
}

如您所见,

Child
模式既有
Parent
附加功能也有它自己的附加功能,其中它自己的附加功能优先。


0
投票

从解决方案@Daniil-Fajnberg 和提到的警告出发,我使用

functools
拼凑了一个快速而肮脏的解决方案。请记住,这只是一个测试示例,并不像接受的答案那么复杂 - 此外,它假设
schema_extra
的所有实例都是可调用的:

from functools import partial

def compose_func(f1, f2, schema, model):
    f1(schema, model)
    f2(schema, model)


def inherit_model_schema_extra(model: type[BaseModel]) -> None:#
    for parent in reversed(model.__mro__):
        if not issubclass(parent, BaseModel):
            continue
        parent_schema_extra = parent.__config__.schema_extra
        if callable(parent_schema_extra):
            model.__config__.schema_extra = partial(compose_func, parent_schema_extra, model.__config__.schema_extra)


class BaseParameterSet(BaseModel):
    def __init_subclass__(cls, **kwargs: object) -> None:
        inherit_model_schema_extra(cls)

    class Config:
        extra = Extra.ignore
        validate_assignment = True
        
        @staticmethod
        def schema_extra(
            schema: dict[str, Any], model: type["BaseParameterSet"]
        ) -> None:
            schema["version"] = "00.00.00"
            schema["info"] = "Parameter set description"
            schema["name"] = "Parameter set Name"


class Child(BaseParameterSet):
    class Config:

        @staticmethod
        def schema_extra(
            schema: dict[str, Any], model: type["BaseParameterSet"]
        ) -> None:
            schema["info"] = "Child Description"
            schema["additional stuff"] = "Something else"
            schema["name"] = f"{schema.get('title')}v{schema.get('version')}"

我只测试了很短一段时间,所以请持保留态度,但我对这种方法的一般意见感兴趣。

© www.soinside.com 2019 - 2024. All rights reserved.