[TOC]
## 第7章 模型對象的序列化
最適合Python JSON序列化的是dict字典類型,每一種語言都有其對應的數據結構用來對應JSON對象,比如在PHP中是它的數組數據結構。而Python是用字典來對應JSON的。如果我們想直接序列化一個對象或者模型對象,那么最笨的辦法是把對象的屬性讀取出來,然后組裝成一個字典再序列化。這實在是太麻煩了。本章節我們將深入了解JSO...
### 7-1 雞湯?
在第6章我們查詢到 User 這個模型對象之后,需要探討的一個問題就是如何將 User 返回到客戶端去。以前我們視圖函數的返回結果要不就是字符串要不就是 APIException,這兩種情況都很好解決,那么如果我們要返回一個模型對象的話,應該怎么辦?
如果我們直接返回模型對象的,實際上就是我們在思維導圖里面說的返回的是業務數據信息,它不是一個異常信息。直接 return user會報錯,如下兩張圖。


在 Python 高級編程中曾經講到過,在 Python 里最適合做序列化的是字典這種數據結構。如果我們要把 user 的相關信息返回去,我們可以嘗試把 user 中的相關數據讀取出來,拼接成字典,再把字典序列化返回到客戶端去。

### 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)`返回。


> `_json.JSONEncoder.default(self, o)`函數
>
> 
那么問題來了:是不是只要調用 `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`的斷點能不能進來?

因為模型里面的屬性非常多,它不是一個簡單的對象,我們可以先定義一個簡單的對象,了解其作用原理之后再來序列化模型。
所以在 ginger/app/api/v1/user.py 中,自定義一個簡單的類進行 `jsonify`

我們打斷點發現并沒有進入自定義的 `default`方法內部,而是進入了原來的`JSONEncoder.default`內部。

顯然這里并沒有實現覆蓋,我們只是定義了,但是 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`,怎么回事兒呢?
> 小知識:
>
> ~~~
> ?class QiYue:
> ? name = 'qiyue'
> ? age = 18
> ~~~
>
> 這里定義的 name、age 是類變量,而不是實例變量,類變量是不會保存在`__dict__`中的,只有實例變量才會保存在`__dict__`中。
我們可以做一個驗證,修改 `QiYue`為:
~~~
?class QiYue:
? ? ?name = 'qiyue'
? ? ?age = 18
??
? ? ?def __init__(self):
? ? ? ? ?self.gender = 'male'
~~~
然后在進行調試可以發現`__dict__`里面有值了,如圖:

到這里對象的實例變量已經可以完成序列化了,那么問題來了,如果我想要把對象的類變量和實例變量都進行序列化,該怎么辦呢?那么顯然這種簡化的`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`。
 調試的時候第一次調用 `default`的時候`o: User 1`:  調試的時候第二次調用 `default`的時候`o: 2019-01-01`,這就是`default`函數的遞歸調用。
 最后,如果大家以后遇到不能序列化的對象就在自定義的`JSONEncoder.default`里面添加 `if`語句來處理。
4. 優化 ginger/app/app.py 文件  上面 `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 是不太考慮業務邏輯的。它不會去考慮前端最終需要的數據格式,反正我只返回一種格式,至于你需要什么格式,你自己看著辦。