在 Marshmallow Schema 中以编程方式定义字段

Define fields programmatically in Marshmallow Schema

假设我有一个这样的架构:

class MySchema(Schema):

    field_1 = Float()
    field_2 = Float()
    ...
    field_42 = Float()

有没有办法以编程方式将这些字段添加到 class?

像这样:

class MyClass(BaseClass):

    FIELDS = ('field_1', 'field_2',..., 'field_42')

    for field in FIELDS:
        setattr(?, field, Float())  # What do I replace this "?" with?

我看过关于动态添加属性到 class 实例的帖子,但这是不同的,因为

同样的问题可能适用于其他模型定义库,例如 ODM/ORM(uMongo/MongoEngine、SQL Alchemy,...)

我设法通过子类化默认元类来做到这一点:

class MySchemaMeta(SchemaMeta):

    @classmethod
    def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls):
        fields = super().get_declared_fields(klass, cls_fields, inherited_fields, dict_cls)
        FIELDS = ('field_1', 'field_2',..., 'field_42')
        for field in FIELDS:
            fields.update({field: Float()})
        return fields

class MySchema(Schema, metaclass=MySchemaMeta):

    class Meta:
        strict = True

我让这个更通用:

class DynamicSchemaOpts(SchemaOpts):

    def __init__(self, meta):
        super().__init__(meta)
        self.auto_fields = getattr(meta, 'auto_fields', [])


class DynamicSchemaMeta(SchemaMeta):

    @classmethod
    def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls):

        fields = super().get_declared_fields(klass, cls_fields, inherited_fields, dict_cls)

        for auto_field_list in klass.opts.auto_fields:
            field_names, field = auto_field_list
            field_cls = field['cls']
            field_args = field.get('args', [])
            field_kwargs = field.get('kwargs', {})
            for field_name in field_names:
                fields.update({field_name: field_cls(*field_args, **field_kwargs)})

        return fields


class MySchema(Schema, metaclass=DynamicSchemaMeta):

    OPTIONS_CLASS = DynamicSchemaOpts

    class Meta:
        strict = True
        auto_fields = [
            (FIELDS,
             {'cls': Float}),
        ]

我没写

class Meta:
    strict = True
    auto_fields = [
        (FIELDS, Float()),
    ]

因为所有这些字段将共享同一个 Field 实例。

Field及其args/kwargs必须分别指定:

    class Meta:
        strict = True
        auto_fields = [
            (FIELDS,
             {'cls': Nested,
              'args': (MyEmbeddedSchema),
              'kwargs': {'required': True}
             }),
        ]

我没有任何示例用例因多个字段共享同一实例而失败,但这听起来不安全。如果这种预防措施没有用,那么可以简化代码并使其更具可读性:

    class Meta:
        strict = True
        auto_fields = [
            (FIELDS, Nested(MyEmbeddedSchema, required=True)),
        ]

显然,此答案特定于 Marshmallow,不适用于其他 ODM/ORM 库。

您需要做的就是使用 type() 函数来构建具有您想要的任何属性的 class:

MySchema = type('MySchema', (marshmallow.Schema,), {
    attr: marshmallow.fields.Float()
    for attr in FIELDS
})

您甚至可以在那里设置不同类型的字段:

fields = {}
fields['foo'] = marshmallow.fields.Float()
fields['bar'] = marshmallow.fields.String()
MySchema = type('MySchema', (marshmallow.Schema,), fields)

或作为您自定义的基础:

class MySchema(type('_MySchema', (marshmallow.Schema,), fields)):
    @marshmallow.post_dump
    def update_something(self, data):
        pass

The class Meta paradigm allows you to specify which attributes you want to serialize. Marshmallow will choose an appropriate field type based on the attribute’s type.

class MySchema(Schema):
    class Meta:
        fields = ('field_1', 'field_2', ..., 'field_42')
    ...

Refactoring: Implicit Field Creation

下面的方法对我有用。

我已经使用 Marshmallow-SQLAlchemy 对其进行了演示,因为我不确定普通 Marshmallow 是否需要这样的东西——在 3.0.0 版中,使用 from_dict 以编程方式创建模式非常简单.但是您当然可以将这些概念用于纯棉花糖。

在这里,我使用 Marshmallow-SQLAlchemy 来推断大部分模式,然后以编程方式对几个字段应用特殊处理。

import enum

from marshmallow_enum import EnumField
from marshmallow_sqlalchemy import ModelSchema
from sqlalchemy import Column
from sqlalchemy import Enum
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.declarative import declarative_base


BaseResource = declarative_base()


class CustomEnum(enum.Enum):
    VALUE_1 = "the first value"
    VALUE_2 = "the second value"


class ExampleResource(BaseResource):
    __tablename__ = "example_resource"

    id = Column(Integer, primary_key=True)
    enum_field = Column(Enum(CustomEnum), nullable=False)
    title = Column(String)
    string_two = Column(String)

    def __init__(self, **kwargs):
        super(ExampleResource, self).__init__(**kwargs)


def generate_schema(class_, serialization_fields, serialization_fields_excluded):
    """A method for programmatically generating schema.

    Args:
        class_ (class): the class to generate the schema for
        serialization_fields (dict): key-value pairs with the field name and its Marshmallow `Field`
        serialization_fields_excluded (tuple): fields to exclude

    Returns:
        schema (marshmallow.schema.Schema): the generated schema

    """

    class MarshmallowBaseSchema(object):
        pass

    if serialization_fields is not None:
        for field, marshmallow_field in serialization_fields.items():
            setattr(MarshmallowBaseSchema, field, marshmallow_field)

    class MarshmallowSchema(MarshmallowBaseSchema, ModelSchema):
        class Meta:
            model = class_
            exclude = serialization_fields_excluded

    return MarshmallowSchema


generated_schema = generate_schema(
    class_=ExampleResource,
    # I'm using a special package to handle the field `enum_field`
    serialization_fields=dict(enum_field=EnumField(CustomEnum, by_value=True, required=True)),
    # I'm excluding the field `string_two`
    serialization_fields_excluded=("string_two",),
)

example_resource = ExampleResource(
    id=1,
    enum_field=CustomEnum.VALUE_2,
    title="A Title",
    string_two="This will be ignored."
)
print(generated_schema().dump(example_resource))
# {'title': 'A Title', 'id': 1, 'enum_field': 'the second value'}

有必要将 MarshmallowBaseSchema 定义为普通对象,添加所有字段,然后从 class 继承,因为 Marshmallow Schema 在 init 上初始化所有字段(特别是 _init_fields()),所以这种继承模式确保所有的字段在那个时候都存在。

您可以使用 marshmallow.Schema.from_dict 生成混合模式。

class MySchema(
    ma.Schema.from_dict({f"field_{i}": ma.fields.Int() for i in range(1, 4)})
):
    field_4 = ma.fields.Str()

如果您使用的是 marshmallow 3 或更高版本,则可以利用 Schema.from_dict 方法。

from marshmallow import Schema, fields

MySchema = Schema.from_dict(
    {
        "id": fields.Str(dump_only=True),
        "content": fields.Str(required=True),
    }
)

如果您的模式的形状需要在 运行 时更改,您可以这样做:

my_schema = {
  "id": fields.Str(dump_only=True),
}

if (some_condition):
    my_schema["additional_field"] = fields.Str(dump_only=True)

MySchema = Schema.from_dict(my_schema)

此示例在 blog post 中有更详细的说明。

@Panic也分享了这个例子,但是答案不完整。