5.模型对象的序列化

一. 理解序列化时的default函数

在上一篇博文中, 我们在get_user视图函中, 并没有返回user:
app/api/v1/user.py

rom app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.user import User

api = Redprint('user')

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid)  # 如果出现异常,我们想要raise我们自己的APIException 所以得重写get_or_404
    return 'i am cannon'

现在我们想要将user以json的格式返回,最好能以return jsonify(user)的形式返回。
查看jsonify的部分源码:flask/json/init.py的default函数:

class JSONEncoder(_json.JSONEncoder):
    """The default Flask JSON encoder.  This one extends the default simplejson
    encoder by also supporting ``datetime`` objects, ``UUID`` as well as
    ``Markup`` objects which are serialized as RFC 822 datetime strings (same
    as the HTTP date format).  In order to support more data types override the
    :meth:`default` method.
    """

    def default(self, o):
        """Implement this method in a subclass such that it returns a
        serializable object for ``o``, or calls the base implementation (to
        raise a :exc:`TypeError`).

        For example, to support arbitrary iterators, you could implement
        default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                return JSONEncoder.default(self, o)
        """
        if isinstance(o, datetime):
            return http_date(o.utctimetuple())
        if isinstance(o, date):
            return http_date(o.timetuple())
        if isinstance(o, uuid.UUID):
            return str(o)
        if hasattr(o, '__html__'):
            return text_type(o.__html__())
        return _json.JSONEncoder.default(self, o)

_json.JSONEncoder.default(self, o)的default源码可以看出是用来报错的, 如下:

    def default(self, o):
        """Implement this method in a subclass such that it returns
        a serializable object for ``o``, or calls the base implementation
        (to raise a ``TypeError``).

        For example, to support arbitrary iterators, you could
        implement default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                # Let the base class default method raise the TypeError
                return JSONEncoder.default(self, o)

        """
        raise TypeError("Object of type '%s' is not JSON serializable" %
                        o.__class__.__name__)

将get_user视图函数修改:

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid)  # 如果出现异常,我们想要raise我们自己的APIException 所以得重写get_or_404
    return jsonify(user)

运行并调试, 发现程序进入了JSONEncoder的default函数, 到达return _json.JSONEncoder.default(self, o)然后报错。并不是所有情况都会进入JSONEncoder的default函数
如果我们将get_user视图函数,改为这样:

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid)  
    r = {'email': user.email,   # 这种方法可以变相实现return user 但并不好。
        'nickname': user.nickname
    }
    return jsonify(r)

程序不会进入JSONEncoder的default函数,也不会报错。JSONEncoder的default函数只有在jsonify传入不能解析的对象时, 才会进入。想要实现jsonify(user),我可以继承并重写JSONEncoder的default函数。


二. 不完美的对象转字典

在app/app.py中继承并修改Flask和JSONEncoder

from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder




class JSONEncoder(_JSONEncoder):
    def default(self, o):
        return o.__dict__    # 打上断点

class Flask(_Flask):
    json_encoder = JSONEncoder


def register_blueprints(app):
    ...


def register_plugin(app):
    ...

def create_app():
    ...

对应修改app/api/v1/user.py

from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.user import User
from flask import jsonify

api = Redprint('user')

class Cannon:
    name = 'cannon'
    age = 26

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    # user = User.query.get_or_404(uid)  
    return jsonify(Cannon)

运行调试, 访问get_user视图函数对应的url, 发现

class JSONEncoder(_JSONEncoder):
    def default(self, o):
        return o.__dict__    # 打上断点

o.__dict__中没有name和age,是空字典, 为什么?
原因: __dict__只存储实例化的变量, name和age是类变量, 不会存储在__dict__中。

介于此,我们使用dict函数代替__dict__


三. 深入理解dict的机制

dict函数, 传入参数如果为对象, 则会调用对象的keys函数,举例:

class Cannon:
    name = 'cannon'
    age = 26
    
    def keys(self):
        return ('name', 'age')   # 类变量  实例变量都可以传入
o = Cannon()
dict(o)

这样dict函数就会相当dict('name'=o['name'], 'age'=o['age']),
但是,对象默认情况无法通过o['name']访问变量name, 我们需要借助__getitem__魔法方法:

class Cannon:
    name = 'cannon'
    age = 26
    
    def keys(self):
        return ('name', 'age')
        
    def __getitem__(self, item):
        return getattr(self, item)
        
o = Cannon()
print(dict(o))

运行得到:

{'name':'cannon', 'age':26}

四. 序列化SQLAlchemy模块

在User模型中加入keys和__getitem__方法

import ...


class User(Base):
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)
    auth = Column(SmallInteger, default=1)
    _password = Column('password', String(100))

    def keys(self):
        return ["auth", "email", "id", "nickname"]

    def __getitem__(self, item):   # __getitem__代码是固定的,之后可以将它放入Base模型中
        return getattr(self, item)

    ...


五. 完善序列化

完善JSONEncoder函数,并将app/app.py重构为 init.py和app.py
app.py

rom flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
from app.libs.error_code import ServerError
from datetime import date


class JSONEncoder(_JSONEncoder):
    def default(self, o):   
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date):     # 类似的遇到无法处理的类, 就单独处理
            return o.strftime('%Y-%m%')
        raise ServerError()


class Flask(_Flask):
    json_encoder = JSONEncoder

__ init __.py

from .app import Flask


def register_blueprints(app):
    from app.api.v1 import create_blueprint_v1
    app.register_blueprint(create_blueprint_v1())


def register_plugin(app):
    from app.models.base import db
    db.init_app(app)
    db.create_all(app=app)


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.config.secure')
    app.config.from_object('app.config.setting')
    register_blueprints(app)
    register_plugin(app)
    return app

app/api/v1/user.py

from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.user import User
from flask import jsonify

api = Redprint('user')

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid) 
    return jsonify(user)   # 直接传入user

运行通过url的BaseAuth+token方法 访问get_user, 得到结果:

{
    "auth": 1,
    "email": "[email protected]",
    "id": 1,
    "nickname": "cannon"
}

六. ViewModel对于API有意义吗

ViewModel 对视图层 提供个性化的视图模型: 
模型的结构不一定适合是前端的格式, 我们需要给前端提供更方便好用的格式。

举例:

前端或api想要展示 userbook两个模型中的一些数据,
我们可以定义一个user_bookviewmodel,将数据统一起来,让视图函数去调用。

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80889323