PEP557 dataclassの仕組みを解説する

## TL;DR

* dataclassすごくいいよ :thumbsup:
* 手書きで書いたclassと遜色ないよ 
* これからdataclassをベースにしたライブラリが出てきそう

## `dataclass`とは?

[dataclass](https://docs.python.org/3/library/dataclasses.html)はpython 3.7で追加された新しい標準ライブラリ。簡単に説明するとclassに宣言に`@dataclass`デコレータを付けると、`__init__`, `__repr__`, `__eq__`, `__hash__`といった所謂dunder(double underscoreの略。日本語だとダンダーと読むのかな)メソッドを生成してくるライブラリ。これを使うと面倒なクラスの定義を大幅に短縮できたり、下手な実装より高速だったりする。ここで紹介した以外にもdataclassには色々な機能があるので、詳しくは[公式ドキュメント](https://docs.python.org/3/library/dataclasses.html)や[Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない](https://qiita.com/tag1216/items/13b032348c893667862a)を参照してほしい。

python3.7がまだ使えないよ、という人にもPyPIに3.6用の[backport](https://pypi.org/project/dataclasses/)が用意されています。

## `dataclass`の使い方

```python
from dataclasses import dataclass, field
from typing import ClassVar, List, Dict, Tuple
import copy

@dataclass
class Foo:
    i: int
    s: str
    f: float
    t: Tuple[int, str, float, bool]
    d: Dict[int, str]
    b: bool = False  # デフォルト値
    l: List[str] = field(default_factory=list)  # listのデフォルトを[]にする
    c: ClassVar[int] = 10  # クラス変数

# 生成された`__init__`でインスタンス化
f = Foo(i=10, s='hoge', f=100.0, b=True,
        l=['a', 'b', 'c'], d={'a': 10, 'b': 20},
        t=(10, 'hoge', 100.0, False))

# 生成された`__repr__`でhの文字列表現をプリントアウトする
print(f)

# コピーを作って書き換えてみる
ff = copy.deepcopy(f)
ff.l.append('d')

# 生成された`__eq__`で比較する
assert f != ff
```

## パフォーマンス

dataclassを使って作ったDataclassFooと手書きで書いたManualFooの`__init__`, `__repr__`, `__eq__`の実行時間を計測してみた。

* macOS 10.14 Mojave
* Intel 2.3GHz 8-core Intel Core i9
* DDR4 32GB RAM
* Python 3.6.3

<details><summary><b><code>計測に使ったソースコード</code></b></summary><div>

```python
import timeit
from dataclasses import dataclass

@dataclass
class DataclassFoo:
    i: int
    s: str
    f: float
    b: bool

class ManualFoo:
    def __init__(self, i, s, f, b):
        self.i = i
        self.s = s
        self.f = f
        self.b = b
    def __repr__(self):
        return f'ManualFoo(i={self.i}, s={self.s}, f={self.f}, b={self.b})'
    def __eq__(self, b):
        a = self
        return a.i == b.i and a.s == b.s and a.f == b.f and a.b == b.b

def bench(name, f):
    times = timeit.repeat(f, number=100000, repeat=5)
    print(name + ':\t' +  f'{sum(t)/5:.5f}')

bench('dataclass __init__', lambda: DataclassFoo(10, 'foo', 100.0, True))
bench('manual class __init__', lambda: ManualFoo(10, 'foo', 100.0, True))

df = DataclassFoo(10, 'foo', 100.0, True)
mf = ManualFoo(10, 'foo', 100.0, True)
bench('dataclass __repr__', lambda: str(df))
bench('manual class __repr__', lambda: str(mf))

df2 = DataclassFoo(10, 'foo', 100.0, True)
mf2 = ManualFoo(10, 'foo', 100.0, True)
bench('dataclass __eq__', lambda: df == df2)
bench('manual class __eq__', lambda: mf == mf2)
```

</div></details>

各10万回を5セット実行した平均

|                          | 計測結果(sec) | 
|--------------------------|---------------|
| dataclass  \_\_init\_\_  | 0.04382       |
| 手書きclass \_\_init\_\_ | 0.04003       |
| dataclass   \_\_repr\_\_ | 0.07527       |
| 手書きclass \_\_repr\_\_ | 0.08414       |
| dataclass   \_\_eq\_\_   | 0.04755       |
| 手書きclass \_\_eq\_\_   | 0.04593       |

50万回実行してこれならほぼ差はないと言っていいでしょう。

また、バイトコードも一致した。

<details><summary><b>dataclassの\_\_init\_\_</b></summary><div>

```python
>>> import dis
>>> dis.dis(DataclassFoo.__init__)
  2           0 LOAD_FAST                1 (i)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (i)

  3           6 LOAD_FAST                2 (s)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (s)

  4          12 LOAD_FAST                3 (f)
             14 LOAD_FAST                0 (self)
             16 STORE_ATTR               2 (f)

  5          18 LOAD_FAST                4 (b)
             20 LOAD_FAST                0 (self)
             22 STORE_ATTR               3 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE
```

</div></details>

<details><summary><b>手書きclassの\_\_init\_\_</b></summary><div>

```python
>>> dis.dis(ManualFoo.__init__)
 13           0 LOAD_FAST                1 (i)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (i)

 14           6 LOAD_FAST                2 (s)
              8 LOAD_FAST                0 (self)
             10 STORE_ATTR               1 (s)

 15          12 LOAD_FAST                3 (f)
             14 LOAD_FAST                0 (self)
             16 STORE_ATTR               2 (f)

 16          18 LOAD_FAST                4 (b)
             20 LOAD_FAST                0 (self)
             22 STORE_ATTR               3 (b)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE
```
</div></details>

## dataclassの内部の解説に入る前に

dataclassを説明するにあたり重要なパーツを説明しておきたい。

### PEP526: Syntax for Variable Annotations

[PEP526](https://www.python.org/dev/peps/pep-0526/)は型宣言の方法を記述してあるんだけど、この仕様追加によってclassに宣言された変数の型情報をプログラム実行時に取得することが可能になった。

```python
from typing import Dict
class Player:
    players: Dict[str, Player]
    __points: int

print(Player.__annotations__)
# {'players': typing.Dict[str, __main__.Player],
#  '_Player__points': <class 'int'>}
```

### 組み込み`exec`関数

evalは知ってる人が多いと思う。ざっくりevalとの違いをいうと、

`eval`: 引数の文字列を式として評価する
`exec`: 引数の文字列を文として評価する

これだけじゃ意味不明なので次の例を見てみよう。

これを実行すると"typing rocks!"を出力されるのは簡単に想像できる。

```python
>>> exec('print("typing rocks!")')
"typing rocks!"
```

ではこれは？

```python
exec('''
def func():
    print("typing rocks!")
''')
```

次にこれを実行してみる

```python
>>> func()
"typing rocks!"
```

そう。実はexecは文字列を式として評価するので、pythonの関数でさえも動的に定義することができる。すげぇ。

## で、dataclassは内部で何を行っているのか？

dataclassデコレータをつけたclassがimportされると、上で説明したtype annotationsやexecを使ってコード生成を行っている。超ざっくりだが、以下のような流れになる。詳しく知りたい人は[cpythonのソースのこの辺](https://github.com/python/cpython/blob/master/Lib/dataclasses.py#L481-L526)を読んでみよう。

1. dataclassデコレータがクラスに対して呼ばれる
2. 各フィールドの型情報（型名、型クラス、デフォルト値等）をtype annotationsから取得する
3. 型情報を使って`__init__` 関数定義の**文字列**を作る
4. 文字列を`exec`に渡して動的に関数を生成する
5. クラスに`__init__`関数をセットする

3, 4, 5を単純化したコードはこんな感じ。

```python
nl = '\n'  # f-string内でエスケープ使えないので外で定義する

# 関数定義の文字列作成
s = f"""
def func(self, {', '.join([f.name for f in fields(Hoge)])}):
{nl.join('  self.'+f.name+'='+f.name for f in fields(Hoge))}
"""

# 関数定義の文字列をコンソール出力してみる
print(s)
# def func(self, i, s, f, t, d, b, l):
#   self.i=i
#   self.s=s
#   self.f=f
#   self.t=t
#   self.d=d
#   self.b=b
#   self.l=l

# execでコード生成。`func`関数がスコープ内に定義された
exec(s)

setattr(Foo, 'func', func)  # クラスに生成した関数をクラスにセットする
```

以上は単純化された例だけど、実際には

* フィールドに設定されたデフォルト値
* List等に使うデフォルトファクトリ関数
* クラス変数(ClassVar)
* プログラマが定義済みだったら生成しない
* 他のdunder関数の生成
* dataclassのclassの継承

等を全て考慮して、どんな場合も正しく動作するように丁寧に丁寧に関数定義文字列作成、コード生成が行われているのです。

さらに、もう一つ押さえておきたいことが、この**コード生成が行われるのはモジュールがロードされた瞬間**のみということ。一度classがimportされたら、**手書きで書いたclassと何の変わりもなく使える**ということだ。

## Rustの`#[derive]`

Rustにはstructを定義する時につけるDerive attribute(`#[derive]`)というものがある。これ、dataclassとほぼ同等かそれ以上のことができる。例えば以下をみてもらえると、

```rust
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Foo {
    i: i32,
    s: String,
    b: bool,
}
```

`#[derive(Debug, Clone, Eq, PartialEq, Hash)]`をつけるだけで、これだけのメソッドを生成してくれる。

* Debug用文字列生成のメソッド生成 (Pythonでいう`__repr__`)
* オブジェクトをクローンするメソッド生成
* 比較メソッド生成(Pythonでいう`__eq__`や`__gt__`)
* ハッシャーメソッド生成(Pythonでいう`__hash__`)

またRustはさらにすごくて、自分のCustom deriveを実装する機能が[公式でサポートされていて](https://doc.rust-lang.org/reference/procedural-macros.html)、割とカジュアルに型ベースのメタプログラミングができる。

こういったプログラマを楽にする機能がRustには他にもたくさんあるので、型制約や所有権が難しくてもRustが生産性が高い理由だと筆者は思っている。[Rustは本当に素晴らしい言語](https://imoz.jp/note/rust-functions.html)なのでPythonistaの方々もぜひぜひ触ってみてほしい。

## メタプログラミングとしてのdataclassの可能性

dataclassは型ベースのメタプログラミングの有用性と可能性を示したいい例だと個人的には思っている。

筆者も二つほどdataclassをベースにしたライブラリを作ってみたので、興味がある人はみてみてほしい。

<a href="https://github.com/yukinarit/envclasses"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/yukinarit/envclasses.png" width="460px"></a>

環境変数の値をdataclassのフィールドにマッピングするライブラリ。コンテナ使ってて、Pythonのコンフィグclassを環境変数でオーバーライドしたい時とかに便利


<a href="https://github.com/yukinarit/pyserde"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/yukinarit/pyserde.png" width="460px"></a>

dataclassベースのシリアライズライブラリ。dataclassを使って、Rustの神ライブラリ[serde](https://github.com/serde-rs/serde)と同等の機能を実装するべく開発中。

## おわりに

RustでそうであるようにPythonでもこの分野が盛り上がって良いライブラリがたくさん出てきてほしい。