dataclassを利用したシリアライズフレームワークpyserdeを紹介する

pyserdeというdataclassを利用したシリアライズフレームワークを開発している。読み方はパイセルデ。

<a href="https://github.com/yukinarit/pyserde"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/224f735c-bcc2-1c1d-8fef-8192f1321dee.png" width="70%"></a>


## TL;DR

普通に定義したdataclassに`@serialize`, `@deserialize`デコレータを付ける

```python
@deserialize
@serialize
@dataclass
class Foo:
    i: int
    s: str
    f: float
    b: bool
```

すると、`to_json`でJSONへシリアライズ。

```python
>> 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からオブジェクトへでシリアライズできる。

```python
>> 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便利だなーと思って[実装](https://github.com/python/cpython/blob/master/Lib/dataclasses.py)を読んだところ、`@dataclass`デコレータを付けたclassがモジュールにロードされた時に[exec関数](https://qiita.com/Kodaira_/items/30c84806b61792b613f2)を使って[メソッドを生成している](https://github.com/python/cpython/blob/550f30c8f33a2ba844db2ce3da8a897b3e882c9a/Lib/dataclasses.py#L377-L401)のが分かりました。あぁこれ面白いなと思って\_\_init\_\_や\_\_repr\_\_、\_\_eq\_\_のパフォーマンス計測してみると手書きclassとほぼ変わらない結果になって、これを使うともっといろんなメソッド生成できるのでは、と思ったのがきっかけです。

execでランタイムに関数を定義する例:

```python
# 文字列で関数を定義する
s = '''
def func():
    print("Hello world!")
'''

# execに文字列を渡す
exec(s)

# 関数がランタイムに定義される！
func()
```

dataclassの仕組みやパフォーマンスについては[こちらの解説](https://qiita.com/yukinarit/items/0dbd81d3533a2b818f20)を参照。

## 名前の由来

Rustに[serde](https://serde.rs/)というシリアライズフレームワークがあります。この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をつけている。

```python
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の場合、このようになる。

```python
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の文字列を指定するだけだ。

```python
s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}'
print(from_json(Foo, s))
```

Yaml, Toml, MsgPackの場合はこんな感じ。

<details><summary>Yaml</summary>
<div>

```python
from serde.yaml import from_yaml, to_yaml
print(to_yaml(f))
print(from_yaml(Foo, s))
```
</div>
</details>

<details><summary>Toml</summary>
<div>

```python
from serde.toml import from_toml, to_toml
print(to_toml(f))
print(from_toml(Foo, s))
```
</div>
</details>

<details><summary>MsgPack</summary>
<div>

```python
from serde.msgpack import from_msgpack, to_msgpack
print(to_msgpack(f))
print(from_msgpack(Foo, s))
```
</div>
</details>



## パフォーマンス

以下の条件で実行時間を計測し、また他のシリアライズライブラリと比較した。 計測用のコードを見たい人は[こちら](https://github.com/yukinarit/pyserde/blob/master/bench/bench.py)を参照

* macOS 10.14 Mojave
* Intel 2.3GHz 8-core Intel Core i9
* DDR4 32GB RAM
* 使用した[class](https://github.com/yukinarit/pyserde/blob/master/bench/dataclasses_class.py#L7-L12) 

JSONへのSerialize/Deserializeそれぞれ10,000回

| Serialize | Deserialize |
|-----------|-------------|
| <img src="https://raw.githubusercontent.com/yukinarit/pyserde/master/bench/charts/se-small.png"> | <img src="https://raw.githubusercontent.com/yukinarit/pyserde/master/bench/charts/de-small.png"> |

Tuple/Dictへの変換それぞれ10,000回

| astuple | asdict|
|-----------|-------------|
| <img src="https://raw.githubusercontent.com/yukinarit/pyserde/master/bench/charts/astuple-small.png"> | <img src="https://raw.githubusercontent.com/yukinarit/pyserde/master/bench/charts/asdict-small.png"> |

チャートは横軸は比較対象で、縦軸はLatencyだ。この棒グラフが低いほどパフォーマンスが良い子になる。チャートを見てもらうと分かるように、pyserdeのパフォーマンスは手書きのrawの次に早い結果となっている。`raw`のコードがpyserdeと比べて関数呼び出し少ないのがパフォーマンスの差になったようだ。

比較対象は以下に一覧にした。

* `raw`: 手書きによるシリアライズ・デシリアライズ。
* `dataclass`: dataclassesのastuple, asdictを使用
* `pyserde`: このライブラリ (Github 7⭐)
* [`dacite`](https://github.com/konradhalas/dacite): Simple creation of data classes from dictionaries. (Github 447️⭐)
* [`mashumaro`](https://github.com/Fatal1ty/mashumaro): Fast and well tested serialization framework on top of dataclasses. (Github 131️⭐)
* [`marshallow`](https://github.com/marshmallow-code/marshmallow): A lightweight library for converting complex objects to and from simple datatypes. (Github 4668⭐)
* [`attrs`](https://github.com/python-attrs/attrs): Python Classes Without Boilerplate. (Github 3076⭐)
* [`cattrs`](https://github.com/Tinche/cattrs): Complex custom class converters for attrs. (Github 214⭐)

その他にもいくつかのライブラリのベンチマークを取ったが、比較にならないほどこれらのライブラリが遅かったので、チャートには載せなかった。
* [`dataclass-json`](https://github.com/lidatong/dataclasses-json): Easily serialize Data Classes to and from JSON (Github 367⭐)
* [`dataclasses_jsonschema`](https://github.com/s-knibbs/dataclasses-jsonschema): JSON schema generation from dataclasses (Github 72⭐)
* [`pavlova`](https://github.com/freelancer/pavlova): A python deserialisation library built on top of dataclasses (Github  28⭐)

※ Githubスター数は2020/5/21日現在

## その他の便利機能

完全に本家serdeの模倣なんですけど、以下の便利機能を実装しています。

### Case Conversion

`snake_case`を`camelCase`や`kebab-case`等に変換する。

```python
@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`になった。

```json
'{"intField": 10, "strField": "foo"}'
```

### Rename Field

フィールド名に例えば`class`のようなキーワードを使いたい場合に便利。

```python
@serialize
@dataclass
class Foo:
    class_name: str = field(metadata={'serde_rename': 'class'})

print(to_json(Foo(class_name='Foo')))
```

classのフィールド名は`class_name`だが、JSONは`class`になった。

```json
{"class": "Foo"}
```

### Skip

フィールドに`serde_skip`を付けると、シリアライズ・デシリアライズから除外できる。

```python
@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`フィールドは除外された。

```json
[{"name": "Stack Overflow", "hash": "hash1"}, {"name": "GitHub", "hash": "hash2"}]
```

### Conditional Skip

指定した条件で除外したい場合、`serde_skip_if`に条件式を渡すことができる。

```python
@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"の時だけ除外されるようになる。

```json
{"player": "satoshi"}
{"player": "green", "buddy": "Charmander"}
```

## 応用例

### 設定ファイルの読み込み

アプリケーションの設定ファイルにYamlやTomlを使うことが多いと思う。pyserdeを使うと簡単に設定ファイルから自分のクラスにマッピングすることができる。

```python
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を使うことによって自分の型にマッピングしたりが容易になる。

<details><summary>Pipfile</summary>
<div>

```toml
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]
pyserde = "~=0.1"
flask = "~=1.1"
```
</div>
</details>

<details><summary>app.py</summary>
<div>

```python
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)
```
</div>
</details><br />


* 実行

```sh
pipenv install
pipenv run python app.py
```


* 全てのToDoを取得する

```sh
$ curl http://localhost:5000/todos
[{"id": 1, "title": "Play games", "description": "Play 聖剣伝説3", "done": false}]⏎
```

* 新しいToDoを登録する

```sh
$ 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
    * https://github.com/yukinarit/pyserde
* Documentation
    * https://yukinarit.github.io/pyserde/