LoginSignup
19
16

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-24

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_casecamelCasekebab-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_casecamelCaseになった。

'{"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

19
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
16