pyserdeというdataclassを利用したシリアライズフレームワークを開発している。読み方はパイセルデ。
TL;DR
普通に定義したdataclassに@serialize
, @deserialize
デコレータを付ける
@deserialize
@serialize
@dataclass
class Foo:
i: int
s: str
f: float
b: bool
すると、to_json
でJSONへシリアライズ。
>> h = Foo(i=10, s='foo', f=100.0, b=True)
>> print(f"Into Json: {to_json(h)}")
Into Json: {"i": 10, "s": "foo", "f": 100.0, "b": true}
from_json
でJSONからオブジェクトへでシリアライズできる。
>> s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}'
>> print(f"From Json: {from_json(Foo, s)}")
From Json: Foo(i=10, s='foo', f=100.0, b=True)
JSONの他に、MsgPack, YAML, Tomlに対応している。その他にいろんな機能あり。
作り始めた動機
Python 3.7に追加されたdataclass便利だなーと思って実装を読んだところ、@dataclass
デコレータを付けたclassがモジュールにロードされた時にexec関数を使ってメソッドを生成しているのが分かりました。あぁこれ面白いなと思って__init__や__repr__、__eq__のパフォーマンス計測してみると手書きclassとほぼ変わらない結果になって、これを使うともっといろんなメソッド生成できるのでは、と思ったのがきっかけです。
execでランタイムに関数を定義する例:
# 文字列で関数を定義する
s = '''
def func():
print("Hello world!")
'''
# execに文字列を渡す
exec(s)
# 関数がランタイムに定義される!
func()
dataclassの仕組みやパフォーマンスについてはこちらの解説を参照。
名前の由来
Rustにserdeというシリアライズフレームワークがあります。このserdeはとにかく神で、Rustが素晴らしい理由の20%ぐらいこいつが占めてると個人的には思っています。
serdeのように便利でハイパフォーマンスで柔軟性のあるフレームワークをPythonで作りたいと思い、pyserdeという名前にしました。
Getting started
インストール
pipでインストール
pip install pyserde
- Windows, macOS, Linux
- Python >= 3.6 (pypyも動作確認済み)
dataclassesはPython 3.7に追加されたが3.6の場合はPyPIにあるdataclassesのバックポートを使用するようになっている。
クラス定義
こんなクラスを作ってみる。ただのdataclassだが、pyserdeの提供する@serialize
、@deserialize
decoratorをつけている。
from serde import serialize, deserialize
from dataclasses import dataclass
@deserialize
@serialize
@dataclass
class Foo:
i: int
s: str
f: float
b: bool
@serialize
を付けるとpyserdeがシリアライズのメソッドを生成し、@deserialize
を付けるとでデシリアライズのメソッドを生成するようになっている。メソッド生成はクラスがPythonインタプリタにロードされた時に一回だけ呼ばれるようになっているので(pyserdeというかデコレータの動き)、クラスを実際に使う際には一切のオーバーヘッドはない。
シリアライズ・デシリアライズしてみる
さて、実際にシリアライズ・デシリアライズしてみよう。pyserdeは0.1.1時点でJSON, Yaml, Toml, MsgPackをサポートしている。各フォーマット用のヘルパー関数はserde.<Format名>
モジュールにあり、命名規則も揃えてる。
例えばJSONの場合、このようになる。
from serde.json import from_json, to_json
to_json
を呼び出し、Foo
のオブジェクトをJSONにシリアライズする
f = Foo(i=10, s='foo', f=100.0, b=True)
print(to_json(f))
でシリアライズする場合はfrom_json
の第一引数にクラスFoo
第二引数にJSONの文字列を指定するだけだ。
s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}'
print(from_json(Foo, s))
Yaml, Toml, MsgPackの場合はこんな感じ。
Yaml
from serde.yaml import from_yaml, to_yaml
print(to_yaml(f))
print(from_yaml(Foo, s))
Toml
from serde.toml import from_toml, to_toml
print(to_toml(f))
print(from_toml(Foo, s))
MsgPack
from serde.msgpack import from_msgpack, to_msgpack
print(to_msgpack(f))
print(from_msgpack(Foo, s))
パフォーマンス
以下の条件で実行時間を計測し、また他のシリアライズライブラリと比較した。 計測用のコードを見たい人はこちらを参照
- macOS 10.14 Mojave
- Intel 2.3GHz 8-core Intel Core i9
- DDR4 32GB RAM
- 使用したclass
JSONへのSerialize/Deserializeそれぞれ10,000回
Serialize | Deserialize |
---|---|
Tuple/Dictへの変換それぞれ10,000回
astuple | asdict |
---|---|
チャートは横軸は比較対象で、縦軸はLatencyだ。この棒グラフが低いほどパフォーマンスが良い子になる。チャートを見てもらうと分かるように、pyserdeのパフォーマンスは手書きのrawの次に早い結果となっている。raw
のコードがpyserdeと比べて関数呼び出し少ないのがパフォーマンスの差になったようだ。
比較対象は以下に一覧にした。
-
raw
: 手書きによるシリアライズ・デシリアライズ。 -
dataclass
: dataclassesのastuple, asdictを使用 -
pyserde
: このライブラリ (Github 7⭐) -
dacite
: Simple creation of data classes from dictionaries. (Github 447️⭐) -
mashumaro
: Fast and well tested serialization framework on top of dataclasses. (Github 131️⭐) -
marshallow
: A lightweight library for converting complex objects to and from simple datatypes. (Github 4668⭐) -
attrs
: Python Classes Without Boilerplate. (Github 3076⭐) -
cattrs
: Complex custom class converters for attrs. (Github 214⭐)
その他にもいくつかのライブラリのベンチマークを取ったが、比較にならないほどこれらのライブラリが遅かったので、チャートには載せなかった。
-
dataclass-json
: Easily serialize Data Classes to and from JSON (Github 367⭐) -
dataclasses_jsonschema
: JSON schema generation from dataclasses (Github 72⭐) -
pavlova
: A python deserialisation library built on top of dataclasses (Github 28⭐)
※ Githubスター数は2020/5/21日現在
その他の便利機能
完全に本家serdeの模倣なんですけど、以下の便利機能を実装しています。
Case Conversion
snake_case
をcamelCase
やkebab-case
等に変換する。
@serialize(rename_all = 'camelcase')
@dataclass
class Foo:
int_field: int
str_field: str
f = Foo(int_field=10, str_field='foo')
print(to_json(f))
snake_case
がcamelCase
になった。
'{"intField": 10, "strField": "foo"}'
Rename Field
フィールド名に例えばclass
のようなキーワードを使いたい場合に便利。
@serialize
@dataclass
class Foo:
class_name: str = field(metadata={'serde_rename': 'class'})
print(to_json(Foo(class_name='Foo')))
classのフィールド名はclass_name
だが、JSONはclass
になった。
{"class": "Foo"}
Skip
フィールドにserde_skip
を付けると、シリアライズ・デシリアライズから除外できる。
@serialize
@dataclass
class Resource:
name: str
hash: str
metadata: Dict[str, str] = field(default_factory=dict, metadata={'serde_skip': True})
resources = [
Resource("Stack Overflow", "hash1"),
Resource("GitHub", "hash2", metadata={"headquarters": "San Francisco"}) ]
print(to_json(resources))
metadata
フィールドは除外された。
[{"name": "Stack Overflow", "hash": "hash1"}, {"name": "GitHub", "hash": "hash2"}]
Conditional Skip
指定した条件で除外したい場合、serde_skip_if
に条件式を渡すことができる。
@serialize
@dataclass
class World:
player: str
buddy: str = field(default='', metadata={'serde_skip_if': lambda v: v == 'Pikachu'})
world = World('satoshi', 'Pikachu')
print(to_json(world))
world = World('green', 'Charmander')
print(to_json(world))
buddy
フィールドが"Pikachu"の時だけ除外されるようになる。
{"player": "satoshi"}
{"player": "green", "buddy": "Charmander"}
応用例
設定ファイルの読み込み
アプリケーションの設定ファイルにYamlやTomlを使うことが多いと思う。pyserdeを使うと簡単に設定ファイルから自分のクラスにマッピングすることができる。
from dataclasses import dataclass
from serde import deserialize
from serde.yaml import from_yaml
@deserialize
@dataclass
class App:
addr: str
port: int
secret: str
workers: int
def main():
with open('app.yml') as f:
yml = f.read()
cfg = from_yaml(App, yml)
print(cfg)
JSON WebAPI
Flask使えばJSON WebAPIはかなり簡単に実装できるが、pyserdeを使うことによって自分の型にマッピングしたりが容易になる。
Pipfile
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pyserde = "~=0.1"
flask = "~=1.1"
app.py
from dataclasses import dataclass
from flask import Flask, request, Response
from serde import serialize, deserialize
from serde.json import to_json, from_json
@deserialize
@serialize
@dataclass
class ToDo:
id: int
title: str
description: str
done: bool
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
@app.route('/todos', methods=['GET', 'POST'])
def todos():
print(request.method)
if request.method == 'GET':
body = to_json([ToDo(1, 'Play games', 'Play 聖剣伝説3', False)])
return Response(body, mimetype='application/json')
else:
todo = from_json(ToDo, request.get_data())
return f'A new ToDo {todo} successfully created.'
if __name__ == '__main__':
app.run(debug=True)
- 実行
pipenv install
pipenv run python app.py
- 全てのToDoを取得する
$ curl http://localhost:5000/todos
[{"id": 1, "title": "Play games", "description": "Play 聖剣伝説3", "done": false}]⏎
- 新しいToDoを登録する
$ curl -X POST http://localhost:5000/todos -d '{"id": 1, "title": "Play games", "description": "Play 聖剣伝説3", "done": false}'
A new ToDo ToDo(id=1, title='Play games', description='Play 聖剣伝説3', done=False) successfully created.⏎
RPC
残念ながらコードを紹介できないが、筆者の会社では独自のRPCフレームワークを持っており、pyserdeを使ってメッセージのMsgPackにシリアライズしている。
References
- Github Repositry
- Documentation