這里看一個最基本的例子,這里給到一個 User 的 Class 定義,再給到一個 data 數據,像這樣:
1 class User(object):2 def __init__(self, name, age):3 self.name =name4 self.age =age5
6 data =[{7 'name': 'Germey',8 'age': 23
9 }, {10 'name': 'Mike',11 'age': 20
12 }]
現在我要把這個 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 的數據類型不同,如何針對不同的數據類型進行針對性的類型轉換,這個你有更好的實現方案嗎?
初步思路
之前我寫過一篇文章這可能是 Python 面向對象編程的最佳實踐,介紹過 attrs 和 cattrs 這兩個庫,它們二者的組合可以非常方便地實現對象的序列化和反序列化。
譬如這樣:
1 fromattr import attrs, attrib2 fromcattr import structure, unstructure3
4 @attrs5 class User(object):6 name =attrib()7 age =attrib()8
9 data ={10 'name': 'Germey',11 'age': 23
12 }13 user =structure(data, User)14 print('user', user)15 json =unstructure(user)16 print('json', json)
運行結果:
1 user User(name='Germey', age=23)2 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,如下:
1 classUserSchema(Schema):2 name =fields.Str()3 age =fields.Integer()4
5 @post_load6 def make(self, data, **kwargs):7 return User(**data)
還是之前的數據:
1 data =[{2 'name': 'Germey',3 'age': 23
4 }, {5 'name': 'Mike',6 'age': 20
7 }]
這時候我們只需要調用 Schema 的 load 事件就好了:
1 schema =UserSchema()2 users = schema.load(data, many=True)3 print(users)
輸出結果如下:
[User(name='Germey', age=23), User(name='Mike', age=20)]
這樣,我們非常輕松地完成了 JSON 到 User List 的轉換。
有人說,如果是單個數據怎么辦呢,只需要把 load 方法的 many 參數去掉即可:
1 data ={2 'name': 'Germey',3 'age': 23
4 }5
6 schema =UserSchema()7 user =schema.load(data)8 print(user)
輸出結果:
User(name='Germey', age=23)
當然,這僅僅是一個反序列化操作,我們還可以正向進行序列化,以及使用各種各樣的驗證條件。
下面我們再來看看吧。
更方便的序列化
上面的例子我們實現了序列化操作,輸出了 users 為:
[User(name='Germey', age=23), User(name='Mike', age=20)]
有了這個數據,我們也能輕松實現序列化操作。
序列化操作,使用 dump 方法即可
1 result = schema.dump(users, many=True)2 print('result', result)
運行結果如下:
result [{'age': 23, 'name': 'Germey'}, {'age': 20, 'name': 'Mike'}]
由于是 List,所以 dump 方法需要加一個參數 many 為 True。
當然對于單個對象,直接使用 dump 同樣是可以的:
1 result =schema.dump(user)2 print('result', result)
運行結果如下:
result {'name': 'Germey', 'age': 23}
這樣的話,單個、多個對象的序列化也不再是難事。
經過上面的操作,我們完成了 object 到 dict 或 list 的轉換,即:
1 object dict2 objects list
驗證
當然,上面的功能其實并不足以讓你覺得 marshmallow 有多么了不起,其實就是一個對象到基本數據的轉換嘛。但肯定不止這些,marshmallow 還提供了更加強大啊功能,比如說驗證,Validation。
比如這里我們將 age 這個字段設置為 hello,它無法被轉換成數值類型,所以肯定會報錯,樣例如下:
1 data ={2 'name': 'Germey',3 'age': 'hello'
4 }5
6 frommarshmallow import ValidationError7 try:8 schema =UserSchema()9 user, errors =schema.load(data)10 print(user, errors)11 except ValidationError ase:12 print('e.message', e.messages)13 print('e.valid_data', e.valid_data)
這里如果加載報錯,我們可以直接拿到 Error 的 messages 和 valid_data 對象,它包含了錯誤的信息和正確的字段結果,運行結果如下:
1 e.message {'age': ['Not a valid integer.']}2 e.valid_data {'name': 'Germey'}
因此,比如我們想要開發一個功能,比如用戶注冊,表單信息就是提交過來的 data,我們只需要過一遍 Validation,就可以輕松得知哪些數據符合要求,哪些不符合要求,接著再進一步進行處理。
當然驗證功能肯定不止這一些,我們再來感受一下另一個示例:
1 frompprint import pprint2 frommarshmallow import Schema, fields, validate, ValidationError3
4 classUserSchema(Schema):5 name = fields.Str(validate=validate.Length(min=1))6 permission = fields.Str(validate=validate.OneOf(['read', 'write', 'admin']))7 age = fields.Int(validate=validate.Range(min=18, max=40))8
9
10 in_data = {'name': '', 'permission': 'invalid', 'age': 71}11 try:12 UserSchema().load(in_data)13 except ValidationError aserr:14 pprint(err.messages)
比如這里的 validate 字段,我們分別校驗了 name、permission、age 三個字段,校驗方式各不相同。
如 name 我們要判斷其最小值為 1,則使用了 Length 對象。permission 必須要是幾個字符串之一,這里又使用了 OneOf 對象,age 又必須是介于某個范圍之間,這里就使用了 Range 對象。
下面我們故意傳入一些錯誤的數據,看下運行結果:
1 {'age': ['Must be greater than or equal to 18 and less than or equal to 40.'],2 'name': ['Shorter than minimum length 1.'],3 'permission': ['Must be one of: read, write, admin.']}
可以看到,這里也返回了數據驗證的結果,對于不符合條件的字段,一一進行說明。
另外我們也可以自定義驗證方法:
1 frommarshmallow import Schema, fields, ValidationError2
3 def validate_quantity(n):4 if n < 0:5 raise ValidationError('Quantity must be greater than 0.')6 if n > 30:7 raise ValidationError('Quantity must not be greater than 30.')8
9 classItemSchema(Schema):10 quantity = fields.Integer(validate=validate_quantity)11
12 in_data = {'quantity': 31}13 try:14 result =ItemSchema().load(in_data)15 except ValidationError aserr:16 print(err.messages)
通過自定義方法,同樣可以實現更靈活的驗證,運行結果:
{'quantity': ['Quantity must not be greater than 30.']}
對于上面的例子,還有更優雅的寫法:
1 frommarshmallow import fields, Schema, validates, ValidationError2
3
4 classItemSchema(Schema):5 quantity =fields.Integer()6
7 @validates('quantity')8 def validate_quantity(self, value):9 if value < 0:10 raise ValidationError('Quantity must be greater than 0.')11 if value > 30:12 raise ValidationError('Quantity must not be greater than 30.')
通過定義方法并用 validates 修飾符,使得代碼的書寫更加簡潔。
必填字段
如果要想定義必填字段,只需要在 fields 里面加入 required 參數并設置為 True 即可,另外我們還可以自定義錯誤信息,使用 error_messages 即可,例如:
1 frompprint import pprint2 frommarshmallow import Schema, fields, ValidationError3
4 classUserSchema(Schema):5 name = fields.String(required=True)6 age = fields.Integer(required=True, error_messages={'required': 'Age is required.'})7 city =fields.String(8 required=True,9 error_messages={'required': {'message': 'City required', 'code': 400}},10 )11 email =fields.Email()12
13 try:14 result = UserSchema().load({'email': 'foo@bar.com'})15 except ValidationError aserr:16 pprint(err.messages)
默認字段
對于序列化和反序列化字段,marshmallow 還提供了默認值,而且區分得非常清楚!如 missing 則是在反序列化時自動填充的數據,default 則是在序列化時自動填充的數據。
例如:
1 frommarshmallow import Schema, fields2 import datetime asdt3 import uuid4
5 classUserSchema(Schema):6 id = fields.UUID(missing=uuid.uuid1)7 birthdate = fields.DateTime(default=dt.datetime(2017, 9, 29))8
9 print(UserSchema().load({}))10 print(UserSchema().dump({}))
這里我們都是定義的空數據,分別進行序列化和反序列化,運行結果如下:
1 {'id': UUID('06aa384a-570c-11ea-9869-a0999b0d6843')}2 {'birthdate': '2017-09-29T00:00:00'}
可以看到,在沒有真實值的情況下,序列化和反序列化都是用了默認值。
這個真的是解決了我之前在 cattrs 序列化和反序列化時候的痛點啊!
指定屬性名
在序列化時,Schema 對象會默認使用和自身定義相同的 fields 屬性名,當然也可以自定義,如:
1 classUserSchema(Schema):2 name =fields.String()3 email_addr = fields.String(attribute='email')4 date_created = fields.DateTime(attribute='created_at')5
6 user = User('Keith', email='keith@stones.com')7 ser =UserSchema()8 result, errors =ser.dump(user)9 pprint(result)
運行結果如下:
1 {'name': 'Keith',2 'email_addr': 'keith@stones.com',3 'date_created': '2014-08-17T14:58:57.600623+00:00'}
反序列化也是一樣,例如:
1 classUserSchema(Schema):2 name =fields.String()3 email = fields.Email(load_from='emailAddress')4
5 data ={6 'name': 'Mike',7 'emailAddress': 'foo@bar.com'
8 }9 s =UserSchema()10 result, errors = s.load(data)
運行結果如下:
1 {'name': u'Mike',2 'email': 'foo@bar.com'}
嵌套屬性
對于嵌套屬性,marshmallow 當然也不在話下,這也是讓我覺得 marshmallow 非常好用的地方,例如:
1 fromdatetime import date2 frommarshmallow import Schema, fields, pprint3
4 classArtistSchema(Schema):5 name =fields.Str()6
7 classAlbumSchema(Schema):8 title =fields.Str()9 release_date =fields.Date()10 artist =fields.Nested(ArtistSchema())11
12 bowie = dict(name='David Bowie')13 album = dict(artist=bowie, title='Hunky Dory', release_date=date(1971, 12, 17))14
15 schema =AlbumSchema()16 result =schema.dump(album)17 pprint(result, indent=2)
這樣我們就能充分利用好對象關聯外鍵來方便地實現很多關聯功能。
以上介紹的內容基本算在日常的使用中是夠用了,當然以上都是一些基本的示例,對于更多功能,可以參考 marchmallow 的官方文檔:https://marshmallow.readthedocs.io/en/stable/,強烈推薦大家用起來。
作者:華為云享專家崔慶才靜覓