使用棉花糖而不重复我自己

Using Marshmallow without repeating myself

根据 Marshmallow 官方文档,建议声明一个架构,然后有一个单独的 class 来接收加载的数据,如下所示:

class UserSchema(Schema):
    name = fields.Str()
    email = fields.Email()
    created_at = fields.DateTime()

    @post_load
    def make_user(self, data):
        return User(**data)

但是,我的 User class 看起来像这样:

class User:
    def __init__(name, email, created_at):
        self.name = name
        self.email = email
        self.created_at = created_at

这似乎是不必要的重复,我真的不喜欢再写三遍属性名称。但是,我确实喜欢 IDE 对定义明确的结构进行自动完成和静态类型检查。

那么,在不定义另一个 class 的情况下,是否有根据 Marshmallow Schema 加载序列化数据的最佳实践?

对于 vanilla Python classes,没有一种开箱即用的方法可以在不重复字段名称的情况下为架构定义 class。

例如,如果您使用的是 SQLAlchemy,则可以使用 marshmallow_sqlalchemy.ModelSchema:

直接从模型定义模式
from marshmallow_sqlalchemy import ModelSchema
from my_alchemy_models import User

class UserSchema(ModelSchema):
    class Meta:
        model = User

同样适用于使用 flask_marshmallow.sqla.ModelSchema.

的 flask-sqlalchemy

对于 vanilla Python classes,您可以定义一次字段并将其用于架构和 model/class:

USER_FIELDS = ('name', 'email', 'created_at')

class User:
    def __init__(self, name, email, created_at):
        for field in USER_FIELDS:
            setattr(self, field, locals()[field])

class UserSchema(Schema):
    class Meta:
        fields = USER_FIELDS

    @post_load
    def make_user(self, data):
        return User(**data)

您必须创建两个 class,但好消息是在大多数情况下您不必多次输入属性名称。如果您使用 Flask、SQLAlchemy 和 Marshmallow,我发现的一件事是,如果您在 Column 定义中定义了一些验证属性,Marshmallow Schema 将自动获取这些以及它们中提供的验证。例如:

import (your-database-object-from-flask-init) as db
import (your-marshmallow-object-from-flask-init) as val

class User(db.Model):
  name = db.Column(db.String(length=40), nullable=False)
  email = db.Column(db.String(length=100))
  created_at = db.Column(db.DateTime)

class UserSchema(val.ModelSchema):
  class Meta:
    model = User

在此示例中,如果您获取数据字典并将其放入 UserSchema().load(data) ,如果在此示例中名称不存在或名称较长,您将看到错误超过 40 个字符,或电子邮件超过 100 个字符。除此之外的任何自定义验证,您仍然需要在您的架构中进行编码。

如果您将模型 class 创建为另一个模型 class 的扩展并继承其属性,它也适用。例如,如果您希望每个 class 都有 created/modified 信息,您可以将这些属性放在父模型 class 中,子模型将继承这些属性及其验证参数。 Marshmallow 不允许您的父模型具有架构,因此我没有关于自定义验证的信息。

我知道您可能已经完成了您的项目,但我希望这对遇到此问题的其他开发人员有所帮助。

相关点列表: 烧瓶 (1.0.2) 烧瓶棉花糖 (0.9.0) Flask-SQLAlchemy (2.3.2) 棉花糖 (2.18.0) 棉花糖-sqlalchemy (0.15.0) SQLAlchemy (1.2.16)

除非您需要反序列化为特定的 class 或者您需要自定义序列化逻辑,否则您可以简单地执行此操作(改编自 https://kimsereylam.com/python/2019/10/25/serialization-with-marshmallow.html):

from marshmallow import Schema, fields
from datetime import datetime

class UserSchema(Schema):
    name = fields.Str(required=True)
    email = fields.Email()
    created_at = fields.DateTime()

schema = UserSchema()
data = { "name": "Some Guy", "email": "sguy@google.com": datetime.now() }
user = schema.load(data)

您还可以在 class 中创建一个函数来创建带有验证规则的字典,尽管它仍然是多余的,但它可以让您将所有内容保留在模型中 class:

class User:
    def __init__(name, email, created_at):
        self.name = name
        self.email = email
        self.created_at = created_at

        @classmethod
        def Schema(cls):
            return {"name": fields.Str(), "email": fields.Email(), "created_at": fields.DateTime()}

UserSchema = Schema.from_dict(User.Schema)

如果您需要强类型和完整的验证功能,请考虑 flask-pydantic 或 marshmallow-dataclass。

marshmallow-dataclass 提供了许多与 marshmallow 类似的验证功能。虽然它有点束缚你的手。它没有对自定义 fields/polymorphism 的内置支持(必须改用 using marshmallow-union)并且似乎不能很好地与 flask-marshmallow 和 marshmallow-sqlalchemy 等堆叠包一起使用。 https://pypi.org/project/marshmallow-dataclass/

from typing import ClassVar, Type
from marshmallow_dataclass import dataclasses
from marshmallow import Schema, field, validate


@dataclass
class Person:
    name: str = field(metadata=dict(load_only=True))
    height: float = field(metadata=dict(validate=validate.Range(min=0)))
    Schema: ClassVar[Type[Schema]] = Schema


Person.Schema().dump(Person('Bob', 2.0))
# => {'height': 2.0}

flask-pydantic 从验证的角度来看不太优雅,但提供了许多相同的功能,并且验证内置于 class 中。请注意,像 min/max 这样的简单验证比在棉花糖中更笨拙。不过,就个人而言,我更喜欢将 view/api 逻辑排除在 class 之外。 https://pypi.org/project/Flask-Pydantic/

from typing import Optional
from flask import Flask, request
from pydantic import BaseModel

from flask_pydantic import validate

app = Flask("flask_pydantic_app")

class QueryModel(BaseModel):
  age: int

class ResponseModel(BaseModel):
  id: int
  age: int
  name: str
  nickname: Optional[str]

# Example 1: query parameters only
@app.route("/", methods=["GET"])
@validate()
def get(query:QueryModel):
  age = query.age
  return ResponseModel(
    age=age,
    id=0, name="abc", nickname="123"
    )