Программное определение полей в Marshmallow Schema

Скажем, у меня есть такая схема:

class MySchema(Schema):

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

Есть ли способ программно добавить эти поля в класс?

Что-то вроде этого:

class MyClass(BaseClass):

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

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

Я видел сообщения о динамическом добавлении атрибутов к экземплярам класса, но это другое, потому что

  • Я не хочу исправлять экземпляр, а класс
  • Marshmallow Schema использует пользовательский метакласс

Тот же вопрос может относиться к другим библиотекам определения моделей, таким как ODM/ORM (uMongo/MongoEngine, SQL Alchemy,...)


person Jérôme    schedule 14.02.2017    source источник


Ответы (5)


Все, что вам нужно сделать, это использовать функцию type() для создания вашего класса с любыми атрибутами, которые вы хотите:

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
person Maxim Kulkin    schedule 15.02.2017
comment
Это замечательно. Он использует только Python, а не специальные механизмы Marshmallow. - person Jérôme; 15.02.2017
comment
Второй вариант полезен и прост в освоении. Спасибо, что поделились :) +1 - person kepy97; 30.07.2020

Мне удалось сделать это, создав подкласс метакласса по умолчанию:

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({fluid: 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 и его аргументы/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.

person Jérôme    schedule 14.02.2017

Парадигма класса Meta позволяет вам указать, какие атрибуты вы хотите сериализовать. Marshmallow выберет соответствующий тип поля на основе типа атрибута.

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

Рефакторинг: неявное создание поля

person radzak    schedule 31.01.2019

У меня работает следующий метод.

Я продемонстрировал это с помощью Marshmallow-SQLAlchemy, потому что я не уверен, что что-то подобное больше нужно для простого Marshmallow — с версией 3.0.0 довольно просто программно создать схему, используя from_dict. Но вы, безусловно, можете использовать эти концепции с простым Marshmallow.

Здесь я использую 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 как простой объект, добавить все поля, а затем наследовать от этого класса, потому что схема Marshmallow инициализирует все поля при инициализации (в частности, _init_fields()), поэтому этот шаблон наследования гарантирует, что все поля присутствуют в то время.

person canary_in_the_data_mine    schedule 14.11.2019

Вы можете использовать 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()
person Panic    schedule 03.03.2021