1. 程式人生 > 程式設計 >Python 序列化和反序列化庫 MarshMallow 的用法例項程式碼

Python 序列化和反序列化庫 MarshMallow 的用法例項程式碼

序列化(Serialization)與反序列化(Deserialization)是RESTful API 開發中繞不開的一環,開發時,序列化與反序列化的功能實現中通常也會包含資料校驗(Validation)相關的業務邏輯。

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()

在很多情況下,我們會有把 Python 物件進行序列化或反序列化的需求,比如開發 REST API,比如一些面向物件化的資料載入和儲存,都會應用到這個功能。

比如這裡看一個最基本的例子,這裡給到一個 User 的 Class 定義,再給到一個 data 資料,像這樣:

class User(object):
 def __init__(self,age):
 self.name = name
 self.age = age
data = [{
 'name': 'Germey','age': 23
},{
 'name': 'Mike','age': 20
}]

現在我要把這個 data 快速轉成 User 組成的陣列,變成這樣:

[User(name='Germey',age=23),User(name='Mike',age=20)]

你會怎麼來實現?

或者我有了上面的列表內容,想要轉成一個 JSON 字串,變成這樣:

[{"name": "Germey","age": 23},{"name": "Mike","age": 20}]

你又會怎麼操作呢?

另外如果 JSON 資料裡面有各種各樣的髒資料,你需要在初始化時驗證這些欄位是否合法,另外 User 這個物件裡面 name、age 的資料型別不同,如何針對不同的資料型別進行鍼對性的型別轉換,這個你有更好的實現方案嗎?

初步思路

之前我寫過一篇文章介紹過 attrs 和 cattrs 這兩個庫,它們二者的組合可以非常方便地實現物件的序列化和反序列化。

譬如這樣:

from attr import attrs,attrib
from cattr import structure,unstructure
 
@attrs
class User(object):
 name = attrib()
 age = attrib()
 
data = {
 'name': 'Germey','age': 23
}
user = structure(data,User)
print('user',user)
json = unstructure(user)
print('json',json)

執行結果:

user User(name='Germey',age=23) json {'name': 'Germey','age': 23}

好,這裡我們通過 attrs 和 cattrs 這兩個庫來實現了單個物件的轉換。

首先我們要肯定一下 attrs 這個庫,它可以極大地簡化 Python 類的定義,同時每個欄位可以定義多種資料型別。

但 cattrs 這個庫就相對弱一些了,如果把 data 換成陣列,用 cattrs 還是不怎麼好轉換的,另外它的 structure 和 unstructure 在某些情景下容錯能力較差,所以對於上面的需求,用這兩個庫搭配起來並不是一個最優的解決方案。

另外資料的校驗也是一個問題,attrs 雖然提供了 validator 的引數,但對於多種型別的資料處理的支援並沒有那麼強大。

所以,我們想要尋求一個更優的解決方案。

更優雅的方案

這裡推薦一個庫,叫做 marshmallow,它是專門用來支援 Python 物件和原生資料相互轉換的庫,如實現 object -> dict,objects -> list,string -> dict,string -> list 等的轉換功能,另外它還提供了非常豐富的資料型別轉換和校驗 API,幫助我們快速實現資料的轉換。

要使用 marshmallow 這個庫,需要先安裝下:

pip3 install marshmallow

好了之後,我們在之前的基礎上定義一個 Schema,如下:

class UserSchema(Schema):
 name = fields.Str()
 age = fields.Integer()
 @post_load
 def make(self,data,**kwargs):
 return User(**data)

還是之前的資料:

data = [{
 'name': 'Germey','age': 20
}]

這時候我們只需要呼叫 Schema 的 load 事件就好了:

schema = UserSchema()
users = schema.load(data,many=True)
print(users)

輸出結果如下:

[User(name='Germey',age=20)]

這樣,我們非常輕鬆地完成了 JSON 到 User List 的轉換。

有人說,如果是單個數據怎麼辦呢,只需要把 load 方法的 many 引數去掉即可:

data = {
 'name': 'Germey','age': 23
}
 
schema = UserSchema()
user = schema.load(data)
print(user)

輸出結果:

User(name='Germey',age=23)

當然,這僅僅是一個反序列化操作,我們還可以正向進行序列化,以及使用各種各樣的驗證條件。

下面我們再來看看吧。

更方便的序列化

上面的例子我們實現了序列化操作,輸出了 users 為:

[User(name='Germey',age=20)]

有了這個資料,我們也能輕鬆實現序列化操作。

序列化操作,使用 dump 方法即可

result = schema.dump(users,many=True)
print('result',result)

執行結果如下:

result [{'age': 23,'name': 'Germey'},{'age': 20,'name': 'Mike'}]

由於是 List,所以 dump 方法需要加一個引數 many 為 True。

當然對於單個物件,直接使用 dump 同樣是可以的:

result = schema.dump(user)
print('result',result)

執行結果如下:

result {'name': 'Germey','age': 23}

這樣的話,單個、多個物件的序列化也不再是難事。

經過上面的操作,我們完成了 object 到 dict 或 list 的轉換,即:

object <-> dict objects <-> list

驗證

當然,上面的功能其實並不足以讓你覺得 marshmallow 有多麼了不起,其實就是一個物件到基本資料的轉換嘛。但肯定不止這些,marshmallow 還提供了更加強大啊功能,比如說驗證,Validation。

比如這裡我們將 age 這個欄位設定為 hello,它無法被轉換成數值型別,所以肯定會報錯,樣例如下:

data = {
 'name': 'Germey','age': 'hello'
}
from marshmallow import ValidationError
try:
 schema = UserSchema()
 user,errors = schema.load(data)
 print(user,errors)
except ValidationError as e:
 print('e.message',e.messages)
 print('e.valid_data',e.valid_data)

這裡如果載入報錯,我們可以直接拿到 Error 的 messages 和 valid_data 物件,它包含了錯誤的資訊和正確的欄位結果,執行結果如下:

e.message {'age': ['Not a valid integer.']} e.valid_data {'name': 'Germey'}

因此,比如我們想要開發一個功能,比如使用者註冊,表單資訊就是提交過來的 data,我們只需要過一遍 Validation,就可以輕鬆得知哪些資料符合要求,哪些不符合要求,接著再進一步進行處理。

當然驗證功能肯定不止這一些,我們再來感受一下另一個示例:

from pprint import pprint
from marshmallow import Schema,fields,validate,ValidationError
class UserSchema(Schema):
 name = fields.Str(validate=validate.Length(min=1))
 permission = fields.Str(validate=validate.OneOf(['read','write','admin']))
 age = fields.Int(validate=validate.Range(min=18,max=40))
in_data = {'name': '','permission': 'invalid','age': 71}
try:
 UserSchema().load(in_data)
except ValidationError as err:
 pprint(err.messages)

比如這裡的 validate 欄位,我們分別校驗了 name、permission、age 三個欄位,校驗方式各不相同。

如 name 我們要判斷其最小值為 1,則使用了 Length 物件。permission 必須要是幾個字串之一,這裡又使用了 OneOf 物件,age 又必須是介於某個範圍之間,這裡就使用了 Range 物件。

下面我們故意傳入一些錯誤的資料,看下執行結果:

{'age': ['Must be greater than or equal to 18 and less than or equal to 40.'],'name': ['Shorter than minimum length 1.'],'permission': ['Must be one of: read,write,admin.']}

可以看到,這裡也返回了資料驗證的結果,對於不符合條件的欄位,一一進行說明。

另外我們也可以自定義驗證方法:

from marshmallow import Schema,ValidationError
def validate_quantity(n):
 if n < 0:
 raise ValidationError('Quantity must be greater than 0.')
 if n > 30:
 raise ValidationError('Quantity must not be greater than 30.')
class ItemSchema(Schema):
 quantity = fields.Integer(validate=validate_quantity)
in_data = {'quantity': 31}
try:
 result = ItemSchema().load(in_data)
except ValidationError as err:
 print(err.messages)

通過自定義方法,同樣可以實現更靈活的驗證,執行結果:

{'quantity': ['Quantity must not be greater than 30.']}

對於上面的例子,還有更優雅的寫法:

from marshmallow import fields,Schema,validates,ValidationError
class ItemSchema(Schema):
 quantity = fields.Integer()
 @validates('quantity')
 def validate_quantity(self,value):
 if value < 0:
  raise ValidationError('Quantity must be greater than 0.')
 if value > 30:
  raise ValidationError('Quantity must not be greater than 30.')

通過定義方法並用 validates 修飾符,使得程式碼的書寫更加簡潔。

必填欄位

如果要想定義必填欄位,只需要在 fields 裡面加入 required 引數並設定為 True 即可,另外我們還可以自定義錯誤資訊,使用 error_messages 即可,例如:

from pprint import pprint
from marshmallow import Schema,ValidationError
class UserSchema(Schema):
 name = fields.String(required=True)
 age = fields.Integer(required=True,error_messages={'required': 'Age is required.'})
 city = fields.String(
 required=True,error_messages={'required': {'message': 'City required','code': 400}},)
 email = fields.Email()
try:
 result = UserSchema().load({'email': '[email protected]'})
except ValidationError as err:
 pprint(err.messages)

預設欄位

對於序列化和反序列化欄位,marshmallow 還提供了預設值,而且區分得非常清楚!如 missing 則是在反序列化時自動填充的資料,default 則是在序列化時自動填充的資料。

例如:

from marshmallow import Schema,fields
import datetime as dt
import uuid
class UserSchema(Schema):
 id = fields.UUID(missing=uuid.uuid1)
 birthdate = fields.DateTime(default=dt.datetime(2017,9,29))
print(UserSchema().load({}))
print(UserSchema().dump({}))

這裡我們都是定義的空資料,分別進行序列化和反序列化,執行結果如下:

{'id': UUID('06aa384a-570c-11ea-9869-a0999b0d6843')} {'birthdate': '2017-09-29T00:00:00'}

可以看到,在沒有真實值的情況下,序列化和反序列化都是用了預設值。

這個真的是解決了我之前在 cattrs 序列化和反序列化時候的痛點啊!

指定屬性名

在序列化時,Schema 物件會預設使用和自身定義相同的 fields 屬性名,當然也可以自定義,如:

class UserSchema(Schema):
 name = fields.String()
 email_addr = fields.String(attribute='email')
 date_created = fields.DateTime(attribute='created_at')
 
user = User('Keith',email='[email protected]')
ser = UserSchema()
result,errors = ser.dump(user)
pprint(result)

執行結果如下:

{'name': 'Keith','email_addr': '[email protected]','date_created': '2014-08-17T14:58:57.600623+00:00'}

反序列化也是一樣,例如:

class UserSchema(Schema):
 name = fields.String()
 email = fields.Email(load_from='emailAddress')
data = {
 'name': 'Mike','emailAddress': '[email protected]'
}
s = UserSchema()
result,errors = s.load(data)

執行結果如下:

{'name': u'Mike','email': '[email protected]'}

巢狀屬性

對於巢狀屬性,marshmallow 當然也不在話下,這也是讓我覺得 marshmallow 非常好用的地方,例如:

from datetime import date
from marshmallow import Schema,pprint
class ArtistSchema(Schema):
 name = fields.Str()
class AlbumSchema(Schema):
 title = fields.Str()
 release_date = fields.Date()
 artist = fields.Nested(ArtistSchema())
bowie = dict(name='David Bowie')
album = dict(artist=bowie,title='Hunky Dory',release_date=date(1971,12,17))
schema = AlbumSchema()
result = schema.dump(album)
pprint(result,indent=2)

這樣我們就能充分利用好物件關聯外來鍵來方便地實現很多關聯功能。

以上介紹的內容基本算在日常的使用中是夠用了,當然以上都是一些基本的示例,對於更多功能,可以參考 marchmallow 的官方文件: https://marshmallow.readthedocs.io/en/stable/,強烈推薦大家用起來 。

總結

到此這篇關於Python 序列化和反序列化庫 MarshMallow 的用法例項程式碼的文章就介紹到這了,更多相關Python 序列化和反序列化庫 MarshMallow 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!