[TOC]
## 第7章 模型对象的序列化
最适合Python JSON序列化的是dict字典类型,每一种语言都有其对应的数据结构用来对应JSON对象,比如在PHP中是它的数组数据结构。而Python是用字典来对应JSON的。如果我们想直接序列化一个对象或者模型对象,那么最笨的办法是把对象的属性读取出来,然后组装成一个字典再序列化。这实在是太麻烦了。本章节我们将深入了解JSO...
### 7-1 鸡汤?
在第6章我们查询到 User 这个模型对象之后,需要探讨的一个问题就是如何将 User 返回到客户端去。以前我们视图函数的返回结果要不就是字符串要不就是 APIException,这两种情况都很好解决,那么如果我们要返回一个模型对象的话,应该怎么办?
如果我们直接返回模型对象的,实际上就是我们在思维导图里面说的返回的是业务数据信息,它不是一个异常信息。直接 return user会报错,如下两张图。
![](https://ws4.sinaimg.cn/large/006tNc79gy1fytt0q1adfj32250u0k20.jpg)
![](https://ws3.sinaimg.cn/large/006tNc79gy1fytt280tmhj31wq0u0ala.jpg)
在 Python 高级编程中曾经讲到过,在 Python 里最适合做序列化的是字典这种数据结构。如果我们要把 user 的相关信息返回去,我们可以尝试把 user 中的相关数据读取出来,拼接成字典,再把字典序列化返回到客户端去。
![](https://ws3.sinaimg.cn/large/006tNc79gy1fyud4vcs0ej31he0f8ach.jpg)
### 7-2 理解序列化时的default函数
上节创建字典序列化的方法比较繁琐,我们可不可以直接序列化 user 对象呢?答案是不可以。但是我们可以重写 jsonify 让其可以直接序列化对象,这就比较高级了。如何重写呢?
首先打开 jsonify 源码 anaconda3/envs/flask\_restful\_api/lib/python3.6/site-packages/flask/json/**init**.py,我们在下面两张图的位置打断点,可以明确知道`jsonify`的断点会进入到 `JSONEncoder.default`内部,参数`o`就是传给`jsonify`的对象,在该函数内部依次比对是否是 `datetime`、`date`、`uuid.UUID`、`__html__`,如果都不是的话,则调用`_json.JSONEncoder.default(self, o)`返回。
![](https://ws4.sinaimg.cn/large/006tNc79gy1fyudk6zz1hj31ro09g0ui.jpg)
![](https://ws2.sinaimg.cn/large/006tNc79gy1fyudjp7uqhj32120ocn2h.jpg)
> `_json.JSONEncoder.default(self, o)`函数
>
> ![](https://ws2.sinaimg.cn/large/006tNc79gy1fyudpytymuj31hq06u75h.jpg)
那么问题来了:是不是只要调用 `jsonify`就一定会调用`JSONEncoder.default`呢?答案为否。
验证一下,这次`jsonify(r)`,事实证明断点并没有走进`JSONEncoder.default`函数内部。
那么什么时候才会调用`JSONEncoder.default`函数呢?
如果 flask 知道如何序列化传入的数据结构的时候,它是不会去调用`JSONEncoder.default`函数的。因为它知道怎么序列化,它就直接帮你序列化了。但是如果说我们要序列化的是一个对象,比如 user 模型。flask 默认是不知道怎么序列化模型的,那么 flask 就回去调用`JSONEncoder.default`函数。
那么 flask 为什么在不能序列化一个数据结构的时候会调用`JSONEncoder.default`函数呢?
原因在于 flask 不知道怎么序列化,但是它可以给我们一个途径,让我们指明这个数据结构应该如何序列化。换句话来说我们需要在`JSONEncoder.default`函数内部,把不能序列化的数据结构转换成能够序列化的数据结构。举个例子来讲,如果说`JSONEncoder.default`这里传进来的是一个 user 对象,user 对象是不能序列化的,如果说我们可以把 user 对象转换成字典,而字典是可以序列化的,那么这样就可以完成 user 对象的序列化了。虽然 user 对象是不能序列化的,但是我们可以将 user 对象的信息读取出来转换成字典,字典是可以序列化的。
本节课我们需要知道`JSONEncoder.default`函数的作用和意义,下节课再来覆写`JSONEncoder.default`方法。
### 7-3 不完美的对象转字典
#### 让 jsonify 调用自定义的 JSONEncoder.default
首先我们肯定是不能修改第三方库包中的源码的,这是愚蠢的行为。我们应该在外部继承`JSONEncoder`,然后用自己定义的 `default`方法覆盖`JSONEncoder.default`方法。
我们现在 ginger/app/app.py 内继承`JSONEncoder`,再自定义 `default`打断点看看 `jsonify`的断点能不能进来?
![](https://ws4.sinaimg.cn/large/006tNc79gy1fyufs5zxmwj31oa0deq4y.jpg)
因为模型里面的属性非常多,它不是一个简单的对象,我们可以先定义一个简单的对象,了解其作用原理之后再来序列化模型。
所以在 ginger/app/api/v1/user.py 中,自定义一个简单的类进行 `jsonify`
![](https://ws2.sinaimg.cn/large/006tNc79gy1fyuep9m6xkj31ww0i641e.jpg)
我们打断点发现并没有进入自定义的 `default`方法内部,而是进入了原来的`JSONEncoder.default`内部。
![](https://ws1.sinaimg.cn/large/006tNc79gy1fyuev6b474j32b80p6dlh.jpg)
显然这里并没有实现覆盖,我们只是定义了,但是 flask 并不知道要去调用我们自己实现的`default`函数,那怎么办呢?我们还需要用我们自己定义的 `JSONEncoder`来代替 flask 原来的`JSONEncoder`,所以我们还需要在 ginger/app/app.py 内做一些改写:
~~~
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
class JSONEncoder(_JSONEncoder):
def default(self, o):
pass
class Flask(_Flask): # 用我们自定义的 Flask 核心对象继承原有的 Flask 核心对象
json_encoder = JSONEncoder # 用我们自定义的 JSONEncoder 替代 Flask 原有的 JSONEncoder
~~~
再次测试就会发现,`jsonify`断点会成功的进入到自定义的`JSONEncoder.default`函数中去。
#### 编写自定义的 JSONEncoder.default
接下来我们需要编写一个具体的业务逻辑来实现自定义的 `default`函数。因为字典是可以被序列化的最合适的类型,所以我们如果能将对象转换成字典就可以了。
那么如何把一个对象转化成字典呢?
答:使用对象内置的方法 `o.__dict__`
但是测试结果返回的确实`{}`空字典。这显然不是我们想要的,我们希望在这里能够显示出我们所定义的对象的姓名和年龄属性(类变量)。但是这里没有实现,这是为什么呢?是我们的思路错了吗?其实我们的思路没有错,只是我们 Python 的细节还是不够注意。这里我们就要分析一下`o.__dict__`到底有没有值?再次调试发现这个 `__dict__`是一个空的字典,没有值。并不是我们想要的`age`和 `name`,怎么回事儿呢?![](https://ws4.sinaimg.cn/large/006tNc79gy1fyug0ypt3wj328u0digpc.jpg)
> 小知识:
>
> ~~~
> class QiYue:
> name = 'qiyue'
> age = 18
> ~~~
>
> 这里定义的 name、age 是类变量,而不是实例变量,类变量是不会保存在`__dict__`中的,只有实例变量才会保存在`__dict__`中。
我们可以做一个验证,修改 `QiYue`为:
~~~
class QiYue:
name = 'qiyue'
age = 18
def __init__(self):
self.gender = 'male'
~~~
然后在进行调试可以发现`__dict__`里面有值了,如图:
![](https://ws3.sinaimg.cn/large/006tNc79gy1fyugd14z40j32c809wjtl.jpg)
到这里对象的实例变量已经可以完成序列化了,那么问题来了,如果我想要把对象的类变量和实例变量都进行序列化,该怎么办呢?那么显然这种简化的`o.__dict__`的方式不行,我们需要另外的方式将所有的变量都转化为字典。下节课再解决这个问题。
### 7-4 深入理解dict的机制
基础问题:出了通过`o.__dict__`的方式把一个对象转换成字典之外还有别的方式能够实现这种转换吗?
因为使用这种方式我们只能拿到对象的实例变量,拿不到对象的类变量。那我们可能需要深入了解一下 `dict()`函数。大多数情况下,创建字典的方法有两种:
1. 第一种
~~~
r = {'nickname': 'qiyue', 'age': 18}
~~~
2. 第二种
~~~
r = dict(nickname='qiyue', age=18)
~~~
`dict`函数的功能是非常强大的,它不仅可以创建字典,它还有很多种其他灵活的运用方式。**如果在调用`dict`函数的时候传入了一个对象,那么 python 会调用该对象下面的一个特殊的方法`keys`。**
python 为什么回去调用 `keys`这个方法呢?
原因就在于我们的目的是生成一个字典,既然要生成字典的话就有两个最重要的因素:键、值。所以调用 `keys`的目的就是为了拿到这个字典里所有的键。至于说些键有哪些,那么完全由我们自己来定义,因为 `keys`方法完全由我们自己来实现。只要 `keys`方法返回的是一种序列类型就是可以的,那么我们可以这么写:
~~~
# 返回元组
def keys(self):
return 'name', 'age', 'gender'
# 返回列表
def keys(self):
return ['name', 'age', 'gender']
~~~
现在一个字典里所有的键我们确定了,那么每一个键里所对应的值该如何确定?
对象会使用`o['name']`、`o['age']`、`o['gender']`的方式来访问键的值,但是这种方式是字典的访问方式,而`o`是对象。那么问题来了对象可以用这种方式来访问呢?默认情况下是不可以的。但是如果我们为类增加一个方法`__getitem__`就可以使用中括号的方式访问类下面的相关变量了。当 python 遇到对象使用中括号方式的时候就会调用`__getitem__`方法,然后把`name`、`age`、`gender`等键的名字当做 `item`参数传入。
如果通过一个`object`下面属性的名字来拿到这个属性的值呢?
~~~
getattr(object, item)
~~~
完整的测试代码如下:
~~~
class QiYue:
name = 'qiyue'
age = 18
def __init__(self):
self.gender = 'male'
@staticmethod
def keys():
return 'name', 'age', 'gender'
def __getitem__(self, item):
return getattr(self, item)
o = QiYue()
print(o['name'], o['age'], o['gender'])
print(o.keys())
print(dict(o))
-------------------------------------------------------------------------
执行结果:
qiyue 18 male
('name', 'age', 'gender')
{'name': 'qiyue', 'age': 18, 'gender': 'male'}
~~~
### 7-5 一个元素的元组要特别注意
上节测试代码中有一个地方需要注意下,就是 `keys`方法 `return`的如果是只有一个元素的元组的时候一定要加逗号`,`,否则会报错
~~~
@staticmethod
def keys():
return 'name',
~~~
如果返回的是列表类型就可以避免这种错误。
### 7-6 序列化SQLAlchemy模型
有了前几节的知识,序列化`user`模型就很简单了。首先将 `get_user`视图函数改为:
~~~
@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
user = User.query.get_or_404(uid)
return jsonify(user)
~~~
其次再修改 ginger/app/models/user.py
~~~
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(128))
def keys(self):
return ['id', 'email', 'nickname', 'auth']
def __getitem__(self, item):
return getattr(self, item)
# 下方代码不需要更改,此处省略
~~~
接着我们使用 postman 测试就行了,毫无疑问此处测试成功。
### 7-7 完善序列化
本节我们来做一些重构:
1. 基本上来说每一个模型类都要进行序列化,所以说每一个模型类里都要写 `keys`、`__getitem__`方法。这就比较烦了,那我们可以优化一下,把一些公共的方法提取到基类里面去。
* `keys`方法比较的个性化,它必须根据不同的模型类来输出不同的属性名称,所以说`keys`不能提取;
* `__getitem__`是可以方法哦 `Base`基类里的。
2. ginger/app/app.py 内我们自定义的`JSONEncoder.default`,写的太简陋了,我们只处理了具有 `keys`、`__getitem__`方法的对象,如果对象没有这两个方法就会报错,所以需要在自定义的`JSONEncoder.default`内部做判断,如果对象没有这两个方法的话就返回 `ServerError`表示服务器内部错误。
~~~
class JSONEncoder(_JSONEncoder):
def default(self, o):
if hasattr(self, 'keys') and hasattr(self, '__getitem__'):
return dict(o)
raise ServerError()
~~~
3. 关于`JSONEncoder.default`方法,还有一个很重要的特性:`default`函数式递归调用的,只要遇到不能序列化的对象就会调用 `default`函数。并且把不能序列化的对象当做`o`传入`default`函数里,让我们来处理。
在我们之前的调试过程中之所以没有遇见`default`是因为我们定义的对象的属性都是一些简单的数据结构。如果遇见对象的属性是另一个对象的话,那么 `default`就会递归调用了。 如下图所示,`User`模型内添加 `time`属性,`datetime`是 python 的 `date` 类型,`keys return` 的地方添加 `time`。
![](https://ws1.sinaimg.cn/large/006tNc79gy1fyuolqwegbj31kc0h6q6v.jpg) 调试的时候第一次调用 `default`的时候`o: User 1`: ![](https://ws2.sinaimg.cn/large/006tNc79gy1fyuombzxdzj31y60bkjue.jpg) 调试的时候第二次调用 `default`的时候`o: 2019-01-01`,这就是`default`函数的递归调用。
![](https://ws1.sinaimg.cn/large/006tNc79gy1fyuomoktitj31rb0bljub.jpg) 最后,如果大家以后遇到不能序列化的对象就在自定义的`JSONEncoder.default`里面添加 `if`语句来处理。
4. 优化 ginger/app/app.py 文件 ![](https://ws3.sinaimg.cn/large/006tNc79gy1fyuozqh1f3j30i809mjry.jpg) 上面 `JSONEncoder`、`Flask`两个类基本上来说是固定不变的,但是 `register_blueprints`、`register_plugins`、`create_app`可能经常需要改动。
* 我们更倾向于将 `JSONEncoder`、`Flask`放在单独的模块文件中作为一个独立的文件,所以将这两个类就放在 app.py 中;
* 把另外三个经常需要改动的函数放到 ginger/app/\_\_init\_\_.py 中比较合适,移动之后需要修改一下依赖导入的问题
### 7-8 ViewModel对于API有意义吗
在flask 高级编程中,我们为每一个模型会建立多个 `ViewMode`,在做网站的时候需要 ViewMode,那么在做 API 的时候还需要 ViewMode 吗?或者说 ViewMode 对于 API 来说有没有意义?
`ViewMode`是为视图层提供个性化的视图模型的。这个视图模型和 sqlalchemy 直接返回回来的视图模型有什么区别呢?
sqlalchemy 返回的模型是原始模型,所谓原始模型就是这个模型下面所有的字段的格式基本上和数据库中存储的数据格式是一摸一样的。
但是数据库里存储的数据格式是前段需要的数据格式吗?显然不一定是。
理论上来说所有的数据都可以在前端进行处理,但是后端不能把所有数据处理化的工作全部都丢给前端,有时候我们需要为前段考虑一下,为前段提供更加方便好用的接口。如果决定返回数据的格式是根据具体的业务来的。原始模型是根据数据库来生成的,它的格式是一定的,但是我们在视图层中或者说在 API 的返回中一定要根据业务具体的个性化参数格式。那这必然存在原始模型向视图模型转化的过程。这个过程就是在`ViewMode`中进行转化。
如果没有`ViewMode` 的话,必然会将原始数据转化成各种各样的数据格式,所以说这样就污染了整个视图函数层。而且我们把具体的转换业务逻辑写在视图函数里是不利于复用的。这只是简单的情况,再复杂一点的就是需要返回的数据是**多模型**数据的组合。合成数据的过程可能相当的复杂,写在视图函数里肯定不合适,但是我们可以定义一个`ViewMode`来处理,在是视图函数里面调用就可以了。
对于严格意义的 RESTful(完全资源化的API)来说的话,`ViewMode`意义不大。因为之前说过了,资源意义上的 RESTful 是不太考虑业务逻辑的。它不会去考虑前端最终需要的数据格式,反正我只返回一种格式,至于你需要什么格式,你自己看着办。