Использование Marshmallow без повторения

Согласно официальной документации Marshmallow, рекомендуется объявить схему, а затем иметь отдельный класс, который получает загруженные данные, например:

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 User:
    def __init__(name, email, created_at):
        self.name = name
        self.email = email
        self.created_at = created_at

Это похоже на излишнее повторение, и мне действительно не нравится писать имена атрибутов еще три раза. Однако мне нравится автодополнение IDE и статическая проверка типов в четко определенных структурах.

Итак, существует ли наилучшая практика загрузки сериализованных данных в соответствии со схемой Marshmallow без определения другого класса?


person Teyras    schedule 23.08.2017    source источник


Ответы (3)


Если вам не нужно десериализовать как определенный класс или вам не нужна пользовательская логика сериализации, вы можете просто сделать это (адаптировано из 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": "[email protected]": datetime.now() }
user = schema.load(data)

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

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. Хотя это как-то связывает руки. Он не имеет встроенной поддержки настраиваемых полей/полиморфизма (вместо этого нужно использовать 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 менее элегантен с точки зрения проверки, но предлагает многие из тех же функций, а проверка встроена в класс. Обратите внимание, что простые проверки, такие как min/max, более неудобны, чем в зефире. Лично я предпочитаю держать логику представления/API вне класса. 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"
    )
person VoteCoffee    schedule 22.03.2021
comment
Если я правильно помню, что я хотел, задавая вопрос, десериализованный результат должен иметь определенный тип для проверки статического типа (mypy, pyright) и автодополнения IDE. - person Teyras; 23.03.2021
comment
@Teyras Я обновил его вторым примером, используя альтернативный пакет, который хранит все в одном классе. Однако это скорее выбор лучшего из двух несовершенных решений. - person VoteCoffee; 23.03.2021
comment
Ха, я думаю, это просто стало лучшим ответом благодаря тому, что библиотеки со временем улучшаются. Я просто хотел бы добавить, что вы также можете использовать классы данных с Pydantic (из коробки) - на самом деле мне такой подход нравится больше. - person Teyras; 24.03.2021

Для ванильных классов Python нет готового способа определить класс для схемы без повторения имен полей.

Например, если вы используете SQLAlchemy, вы можете определить схему непосредственно из модели с помощью marshmallow_sqlalchemy.ModelSchema:

from marshmallow_sqlalchemy import ModelSchema
from my_alchemy_models import User

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

То же самое относится к flask-sqlalchemy, который использует flask_marshmallow.sqla.ModelSchema.

В случае ванильных классов Python вы можете определить поля один раз и использовать их как для схемы, так и для модели/класса:

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)
person Moses Koledoye    schedule 23.08.2017
comment
Определение класса с помощью setattr не позволяет мне указывать правила проверки, поэтому я все-таки заморачиваюсь с Marshmallow. Я мог бы использовать collections.namedtuple, но это кажется довольно неуклюжим, когда задействованы вложенные схемы. - person Teyras; 23.08.2017
comment
@Teyras В таких случаях, я думаю, вам придется повторить поля. Документы явно делают это. - person Moses Koledoye; 23.08.2017

Вам придется создать два класса, но хорошая новость заключается в том, что в большинстве случаев вам не придется вводить имена атрибутов несколько раз. Одна вещь, которую я обнаружил, если вы используете Flask, SQLAlchemy и Marshmallow, заключается в том, что если вы определяете некоторые атрибуты проверки в своем определении столбца, схема Marshmallow автоматически выберет их и предоставленные в них проверки. Например:

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 символов. Любые пользовательские проверки помимо этого вам все равно придется кодировать в вашей схеме.

Это также работает, если вы создали класс модели как расширение другого класса модели, перенеся его атрибуты. Например, если вы хотите, чтобы каждый класс создавал/изменял информацию, вы можете поместить эти атрибуты в родительский класс модели, и дочерний класс унаследует их вместе с их параметрами проверки. Marshmallow не позволяет вашей родительской модели иметь схему, поэтому у меня нет информации о пользовательских проверках.

Я знаю, что вы, вероятно, уже завершили свой проект, но я надеюсь, что это поможет другим разработчикам, столкнувшимся с этим.

Соответствующий список пунктов: Flask (1.0.2) flask-marshmallow (0.9.0) Flask-SQLAlchemy (2.3.2) marshmallow (2.18.0) marshmallow-sqlalchemy (0.15.0) SQLAlchemy (1.2.16)

person user3832673    schedule 22.01.2019
comment
Это довольно интересно! Это новая функция Marshmallow 3? Никак не могу найти документацию на него... - person Teyras; 26.01.2019
comment
Собираюсь обновить свой пост соответствующим списком пипсов, чтобы вы могли видеть, на каких версиях я работаю. Это не 3 версия. Сам был удивлен, хоть и приятно. - person user3832673; 28.01.2019
comment
О, функция, которую вы используете, предоставлена ​​Flask-Marshmallow (flask-marshmallow.readthedocs. io/ru/последние)! Теперь я понимаю, почему об этом нет упоминания в документах Marshmallow. В любом случае, спасибо за ваш ответ! - person Teyras; 28.01.2019