Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない

# はじめに

Python3.7が2018/06/15にリリースされる予定です。
https://www.python.org/dev/peps/pep-0537/#release-schedule

Python3.7の新機能に Data Classes がありますが、これを使いこなせばクラス定義が楽になりそうです。

尚、Python3.7の環境作成は以下を参照して下さい。

[もうすぐリリースされるPython3.7環境をDockerで作る - Qiita](https://qiita.com/tag1216/items/949befbbf6d4030ab0b3)

# Data Classes とは

データを格納するためのクラスを簡単に定義できる機能です。
クラス定義にデコレータを１つ付けるだけで`__init__`や`__str__`などの特殊メソッドを自動生成してくれます。

https://docs.python.org/ja/3.7/library/dataclasses.html

# 基本的な使い方

- クラス定義に`dataclass`デコレータを付ける
- クラス変数でフィールドを定義する

```python:クラスの定義
import dataclasses

@dataclasses.dataclass
class User:
    name: str
    age: int = 0
```

```python:クラスを使う
User('tarou', 99)
Out[3]: User(name='tarou', age=99)

User('hanako')
Out[4]: User(name='hanako', age=0)

user1 = User('tarou')
user1.age = 99
user1.name, user1.age
Out[5]: ('tarou', 99)

user1 == User('hanako', 99)
Out[6]: False

user1 == User('tarou', 99)
Out[7]: True
```

# カスタマイズ

## メソッドを定義する

通常のクラスと同じようにメソッドを定義できます。

```python
import dataclasses

@dataclasses.dataclass
class User:
    name: str
    age: int = 0

    def format(self):
        return f'{self.name}さん({self.age}歳)'
```

## イミュータブルにする

デフォルトではミュータブル(変更可能)なオブジェクトになりますが、`frozen=True`にするとイミュータブル(変更不可)なオブジェクトになります。

```python
@dataclasses.dataclass(frozen=True)
class User:
    name: str
    age: int = 0

user1 = User('tarou')
user1.age = 99

FrozenInstanceError                       Traceback (most recent call last)
<ipython-input-60-cba1cafa11e3> in <module>()
----> 1 user1.age = 99

<string> in __setattr__(self, name, value)

FrozenInstanceError: cannot assign to field 'age'
```

## ミュータブルなデフォルト値を使う

```python
@dataclasses.dataclass
class User:
    name: str
    age: int = 0
    items: List[int] = dataclasses.field(default_factory=list)

User('tarou')

Out[12]: User(name='tarou', age=0, items=[])
```

## フィールドを比較対象から除外する

```python
@dataclasses.dataclass
class User:
    name: str
    age: int = dataclasses.field(default=0, compare=False)

User('tarou', 10) == User('tarou', 20)
Out[15]: True
```

## フィールドを`__init__`の引数から除外する

```python
@dataclasses.dataclass
class User:
    name: str
    age: int = dataclasses.field(default=0, init=False)

User('tarou')

Out[19]: User(name='tarou', age=0)

User('tarou', age=99)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-ab6e147bf66a> in <module>()
----> 1 User('tarou', age=99)

TypeError: __init__() got an unexpected keyword argument 'age'
```

## 自動生成された`__init__`の後に処理を入れる

`__post_init__`という名前のメソッドを定義すると`__init__`の後に呼ばれます。

```python
@dataclasses.dataclass
class User:
    name: str
    age: int = 0
    def __post_init__(self):
        print('__post_init__', self.name, self.age)

User('tarou')
__post_init__ tarou 0
Out[24]: User(name='tarou', age=0)
```

## `__init__`のみで使用する変数を指定する

クラス変数で型を`dataclasses.InitVar`にすると、`__init__`でのみ使用するパラメータになります。
`dataclasses.InitVar`で定義したクラス変数はフィールドとは認識されずインスタンスには保持されません。

```python
@dataclasses.dataclass
class User:
    name: str = dataclasses.field(init=False)
    age: int = dataclasses.field(init=False)
    values: dataclasses.InitVar[Tuple[str, int]] = None
    def __post_init__(self, values):
        self.name, self.age = values

User(values=('tarou', 99))

Out[28]: User(name='tarou', age=99)
```

# ユーティリティ関数

## フィールドリストを取得する

```python
dataclasses.fields(User)

Out[22]:
(Field(name='name',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x7fd864bfc080>,default_factory=<dataclasses._MISSING_TYPE object at 0x7fd864bfc080>,init=True,repr=True,hash=None,compare=True,metadata={}),
 Field(name='age',type=<class 'int'>,default=0,default_factory=<dataclasses._MISSING_TYPE object at 0x7fd864bfc080>,init=False,repr=True,hash=None,compare=True,metadata={}))
```

## dictに変換する

```python
dataclasses.asdict(User('tarou', 99))

Out[8]: {'name': 'tarou', 'age': 99}
```

## tupleに変換する

```python
dataclasses.astuple(User('tarou', 99))

Out[9]: ('tarou', 99)
```

# 最後に

[基本的な使い方](#基本的な使い方)だけだと [namedtuple](https://docs.python.org/ja/3.6/library/collections.html#collections.namedtuple) と大きな違いはないのですが、Data Classes では色々とカスタマイズができることがわかりました。
今後クラス定義は Data Classes を使うのが標準的になりそうな予感がします。