Flask RESTful API開發之序列化與反序列化

Gevin發表於2016-07-16

Flask Serialization

序列化(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的作用是,把dictlist轉換為string(類似於json.dumps()),同時,在response的header中,新增content-type =application/json,由於RESTful API通常以json格式作為載體,該方法算是一個shortcut。

由此,物件的序列化,只要實現object -> dictobjects -> list 即可。

反序列化,通常分兩步實現:

  1. 將request中的文字資料轉換為python原生的dict或list
  2. 基於dict為初始化資料,建立類的例項

反序列化實現的關鍵是上面第一步,而這一步實現非常容易,如果資料載體為 json,只需呼叫 request.get_json()方法,即可將傳入的文字資料轉換為dict或list。

2. Marshmallow

Marshmallow 是一個強大的輪子,很好的實現了 object -> dictobjects -> liststring -> dictstring -> 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 -> dictdumps()方法實現 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 validationschema level validation,建立schema時,實現必要Validation是必須的,由於詳細闡述佔用的篇幅會比較長,這部分內容請大家直接檢視官方文件:

Partial Loading

按照RESTful架構風格的要求,更新資料使用HTTP方法中的PUTPATCH方法,使用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-SQLAlchemymarshmallow-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聯絡




如果您覺得Gevin的文章有價值,就請Gevin喝杯茶吧!

|

歡迎關注我的微信公眾賬號

Flask RESTful API開發之序列化與反序列化

相關文章