Используя Зефир, не повторяя себя

Согласно официальным документам 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 и статическая проверка типов в четко определенных структурах.

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

3 ответа

Решение

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

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

from marshmallow_sqlalchemy import ModelSchema
from my_alchemy_models import User

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

То же относится и к колбе-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)

Вам нужно будет создать два класса, но хорошая новость заключается в том, что в большинстве случаев вам не придется вводить имена атрибутов несколько раз. Одна вещь, которую я обнаружил, если вы используете Flask, SQLAlchemy и Marshmallow, это то, что, если вы определите некоторые атрибуты проверки в своем определении Column, схема 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 символов. Любые пользовательские проверки, кроме того, что вам все равно придется кодировать в вашей схеме.

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

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

Соответствующий список пипсов: 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)

Если вам не нужно десериализовать как конкретный класс или вам нужна настраиваемая логика сериализации, вы можете просто сделать это (адаптировано из 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)

Вы также можете создать функцию в своем классе, которая создает 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. Он менее элегантен с точки зрения проверки, но предлагает многие из тех же функций, а проверка встроена в класс. Обратите внимание, что простые проверки, такие как min / max, более неудобны, чем в зефире. Лично я предпочитаю держать логику view / 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"
    )
Другие вопросы по тегам