序列化(Serialization)與反序列化(Deserialization)是RESTful API 開發中繞不開的一環,開發時,序列化與反序列化的功能實現中通常也會包含資料校驗(Validation)相關的業務邏輯。本文結合我的實踐經驗,介紹一種Flask RESTful API開發中實現序列化和反序列化的方法,如果想了解更多相關的理論要點,可以參考Gevin的部落格之前的文章。
1. Flask 相關功能基礎
python 內建資料型別中的dict和list,可以直接序列化為文字,如:
# Dict和list能夠直接被序列化為文字,如下:
def test_list():
data = [{'a':1, 'b':2}, {'c':3, 'd':4}]
return jsonify(result=data)
def test_dict1():
data = {'a':1, 'b':2, 'c':3}
return jsonify(data)
def test_dict2():
data = {'a':1, 'b':2, 'c':3}
return jsonify(**data)
def test_dict3():
data = {'a':1, 'b':2, 'c':3}
return jsonify(result=data)
其中,jsonify
的作用是,把dict
或list
轉換為string
(類似於json.dumps()
),同時,在response的header中,新增content-type =application/json
,由於RESTful API通常以json
格式作為載體,該方法算是一個shortcut。
由此,物件的序列化,只要實現object -> dict
或 objects -> list
即可。
反序列化,通常分兩步實現:
- 將request中的文字資料轉換為python原生的dict或list
- 基於dict為初始化資料,建立類的例項
反序列化實現的關鍵是上面第一步,而這一步實現非常容易,如果資料載體為 json
,只需呼叫 request.get_json()
方法,即可將傳入的文字資料轉換為dict或list。
2. Marshmallow
Marshmallow 是一個強大的輪子,很好的實現了 object -> dict
, objects -> list
, string -> dict
和 string -> list
。
Marshmallow is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes.
-- From marshmallow官方文件
Marshmallow的使用,將從下面幾個方面展開,在開始之前,首先需要一個用於序列化和反序列化的類,我直接與marshmallow官方文件保持一致了:
class User(object):
def __init__(self, name, email):
self.name = name
self.email = email
self.created_at = dt.datetime.now()
Schema
要對一個類(記為Class_A
,以便表達)進行序列化和反序列化,首先要建立一個與之對應的類(記Class_A'
),負責實現Class_A
的序列化、序列化和資料校驗等,Class_A'
就是schema,即:
Schema是序列化功能的載體,每個需要被序列化或反序列化的類,都要設計一個相應的Schema,以實現相關功能。Schema中的欄位,是被序列化的類的對映,如:
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
建立schema
時,schema中的欄位必須與類的成員命名一致,不一致的欄位無法被序列化和反序列化。
序列化
序列化使用schema中的dump()
或dumps()
方法,其中,dump()
方法實現obj -> dict
,dumps()
方法實現 obj -> string
,由於Flask能直接序列化dict,所以通常Flask與Marshmallow配合序列化時,用 dump()
方法即可。
from marshmallow import pprint
user = User(name="Monty", email="monty@python.org")
schema = UserSchema()
result = schema.dump(user)
pprint(result.data)
json_result = schema.dumps(user)
pprint(json_result.data)
反序列化
反序列化基於schema中的load()
或loads()
方法,預設情況下,load()
方法將一個傳入的dict
,結合schema的約定,再轉換為一個dict
,而loads()
方法的傳入引數是json格式的string
,同樣將傳入資料轉換為符合規範的dict
。由於呼叫load()
或loads()
方法時,會執行下面提到的資料校驗
,所以在開發RESTful API時,對傳入資料執行load()
或loads()
方法是必要的。load()
方法使用如下:
from pprint import pprint
user_data = {
'created_at': '2014-08-11T05:26:03.869245',
'email': u'ken@yahoo.com',
'name': u'Ken'
}
schema = UserSchema()
result = schema.load(user_data)
pprint(result.data)
對反序列化而言,將傳入的dict
變成object
更加有意義。在Marshmallow中,dict -> object
的方法需要自己實現,然後在該方法前面加上一個decoration:post_load
即可,即:
from marshmallow import Schema, fields, post_load
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
@post_load
def make_user(self, data):
return User(**data)
這樣每次呼叫load()
方法時,會按照make_user
的邏輯,返回一個User類
物件。
user_data = {
'name': 'Ronnie',
'email': 'ronnie@stones.com'
}
schema = UserSchema()
result = schema.load(user_data)
result.data # => <User(name='Ronnie')>
Objects <-> List
上面的序列化和反序列化,是針對一個object而言的,對於objects的處理,只需在schema中增加一個引數:many=True
,即:
user1 = User(name="Mick", email="mick@stones.com")
user2 = User(name="Keith", email="keith@stones.com")
users = [user1, user2]
# option 1:
schema = UserSchema(many=True)
result = schema.dump(users)
# Option 2:
schema = UserSchema()
result = schema.dump(users, many=True)
result.data
Validation
Marshmallow中的Validation功能用於校驗客戶端傳入的資料是否規範,通常用於建立和修改資料。
Validation可分為 field level validation
和 schema level validation
,建立schema時,實現必要Validation是必須的,由於詳細闡述佔用的篇幅會比較長,這部分內容請大家直接檢視官方文件:
Partial Loading
按照RESTful架構風格的要求,更新資料使用HTTP方法中的PUT
或PATCH
方法,使用PUT
方法時,需要把完整的資料全部傳給伺服器,使用PATCH
方法時,只需把需要改動的部分資料傳給伺服器即可。因此,當使用PATCH
方法時,傳入資料存在無法通過Marshmallow 資料校驗的風險,為了避免這種情況,需要藉助Partial Loading功能。
實現Partial Loadig只要在schema中增加一個partial
引數即可:
class UserSchema(Schema):
name = fields.String(required=True)
age = fields.Integer(required=True)
data, errors = UserSchema().load({'age': 42}, partial=True)
# OR UserSchema(partial=True).load({'age': 42})
data, errors # => ({'age': 42}, {})
Context
關於Marshmallow,最後想提的一點是,context
:
The context attribute of a Schema is a general-purpose store for extra information that may be needed for (de)serialization. It may be used in both Schema and Field methods.
Context
是很用的,比如,更新資料時,更新的是一個資料庫中已有的物件,反序列化操作如果能基於該物件來反序列化就再好不過了,實現這個小需求一種可行方案是,把該物件放到context中,schema中針對該物件實現相關業務邏輯。
3. Marshmallow 與 Flask 的結合
Marshmallow有一個flask extention:flask-marshmallow,該擴充套件與Flask-SQLAlchemy
和marshmallow-sqlalchemy
有整合,有興趣的同學不妨研究一下。
對Gevin而言,資料庫抽象層通常使用Flask-MongoEngine,marshmallow本身就夠用了。
Talk is cheap, show me the code.
關於Marshmallow在Flask中的應用,大家可以檢視我在GitHub上的這兩個程式碼檔案:
以上兩個程式碼檔案來自我尚在開發中的專案JuneMessage,RESTful API和schema部分已經完成,可以參考一下。另外,schema部分不僅把上面提到的要點均實現了一遍,在程式碼的組織上也更進一步,看程式碼比單純看上面的文字應該更有意義。
注:
Flask 功能基礎部分,附加一個說明。
需要注意的是,處於安全考慮,Flask不允許直接返回list
,即:
data = [{'a':1, 'b':2}, {'c':3, 'd':4}]
# This is OK
@app.route('/list1/')
def test_list():
return jsonify(result=data)
# This fails
@app.route('/list1/')
def test_list():
return jsonify(data)
由於這段內容不是本文主題,所以放到最後做補充說明。
RESTful 架構風格相關文章:
注:轉載本文,請與Gevin聯絡
歡迎關注我的微信公眾賬號
