一. 理解序列化时的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想要展示 user和book两个模型中的一些数据,
我们可以定义一个user_book的viewmodel,将数据统一起来,让视图函数去调用。