在 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 实例的帖子,但这是不同的,因为
- 我不想修补一个实例,而是 class
- Marshmallow Schema 使用自定义元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')
...
下面的方法对我有用。
我已经使用 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也分享了这个例子,但是答案不完整。
假设我有一个这样的架构:
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 实例的帖子,但这是不同的,因为
- 我不想修补一个实例,而是 class
- Marshmallow Schema 使用自定义元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')
...
下面的方法对我有用。
我已经使用 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也分享了这个例子,但是答案不完整。