Python3.7からのdataclassデコレータの引数

[このアドベントカレンダー](https://qiita.com/advent-calendar/2018/renttle)でも何度か登場している **dataclassデコレータの引数** をまとめます。

## 目次
- dataclassデコレータの引数紹介
- init
- repr
- eq
- order
- unsafe_hash
- frozen
- おまけ

## 対象
- dataclassデコレータを使ってみたい人
- dataclassデコレータを使ってるけど、引数をいじったことがない人

## dataclassデコレータの引数紹介
引数は、6つ。

|引数名|デフォルト値|
| :---: | :---: |
| init | True |
| repr | True |
| eq | True |
| order | False |
| unsafe_hash | False |
| frozen | False |

## init

### __ init __()の記述なしでインスタンス化できる

```python:sample.py
@dataclass()
class UserStatus:
    username: str
    address: str

if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya")
    print(user_status)

# => UserStatus(username='kota', address='Nagoya')
```

### initを`False`に変更する

```python:sample.py
@dataclass(init=False)
class UserStatus:
    username: str
    address: str
    age: int # インスタンス変数を追加

    def __init__(self, username, address):
        self.username = username
        self.address = address

if __name__ == "__main__":
    user_status = UserStatus(username="kota", address="Nagoya")
```

dataclassのインスタンス変数の定義の仕方に従って、`age`という変数を追加した。そして、インスタンス化時に`age`を渡さないとどうなるでしょうか。
実行結果は、`TypeError: __init__() missing 1 required positional argument: 'age'`のエラーにはなりません。

そこで、インスタンスのageプロパティを呼ぶとどうなるだろう。

```python:sample.py
print(user_status.age)
# AttributeError: 'UserStatus' object has no attribute 'age'
```

つまり、インスタンス化はできるが、`ageプロパティ`がないと怒られます。

ここからわかることは、`init=False`にすると、クラス名の直下に定義されている、インスタンス変数「username: str」「address: str」「age: int」は、無視されているようです。

## repr
クラス名、各プロパティ名が、クラス内で定義された順序で並びます。

```python:sample.py
@dataclass()
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)

    print("__repr__(): ", user_status.__repr__())
    # => __repr__():  UserStatus(username='kota', address='Nagoya', age=23)
```

引数内で`repr=False`とすると、`__repr__():  <__main__.UserStatus object at 0x1073be6a0>`と表示され、プロパティは表示されません。

## eq
インスタンス同士やインスタンス変数同士が等しいかどうかを比較することができます。

```python:sample.py
@dataclass()
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)
    user_status_2 = UserStatus("yuki", "Nagoya", 24)

    print(user_status.__eq__(user_status_2))
    # => False
    print(user_status.address.__eq__(user_status_2.address))
    # => True
```

### デフォルト値を`False`に変更する

```python:sample.py
@dataclass(eq=False)
class UserStatus:
    # 省略

if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)
    user_status_2 = UserStatus("yuki", "Nagoya", 24)

    print(user_status.__eq__(user_status_2))
    # => NotImplemented
    print(user_status.address.__eq__(user_status_2.address))
    # => True
```

`eq`のデフォルト値を`False`に変更して、先程と同内容を比較する。
すると、インスタンス変数同士の比較は、正しく行ってくれるが、インスタンス同士の比較は、行われずに`NotImplemented`と出力されます。

「公式ドキュメントの[データモデル](https://docs.python.org/ja/3/reference/datamodel.html#object.__eq__)」には、このように書かれています。

> 拡張比較メソッドは与えられた引数のペアに対する演算を実装していないときに、 シングルトン NotImplemented を返すかもしれません。

なので、dataclassで`eq=False`の状態で、インスタンス同士の比較をしたい際は、自分で実装しないといけないです。だったら、デフォルト値のままにしておいて、比較できる状態にしておく方が、便利ですよねw

## order
`order`がデフォルト値のまま、`less than`や`greater than`を使って比較してみます。

```python:sample.py
@dataclass()
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)
    user_status_2 = UserStatus("yuki", "Nagoya", 24)

    print("UserStatusインスタンス同士の比較: ", user_status.__lt__(user_status_2))
    # => UserStatusインスタンス同士の比較:  NotImplemented
    print("age同士の比較: ", user_status.age.__gt__(user_status_2.age))
    # => age同士の比較:  False
```

[上記](#eq)と同じ理由で、インスタンス同士の比較はうまくできません。そこで、`order=True`にしてみます。
### デフォルト値を`True`に変更する

```python:sample.py
@dataclass(order=True)
class UserStatus:
    # 省略

if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)
    user_status_2 = UserStatus("yuki", "Nagoya", 24)

    print("UserStatusインスタンス同士の比較: ", user_status.__lt__(user_status_2))
    # => UserStatusインスタンス同士の比較:  True
```

今度は正常に比較してくれました。`order`は`True`の方が便利な場面が多そうです。

## unsafe_hash

> eq と frozen が両方とも真だった場合、デフォルトでは dataclass() は __ hash __ () メソッドを生成します。 eq が真で frozen が偽の場合、__ hash __ () は None に設定され、(可変なので) ハッシュ化不可能とされます。 eq が偽の場合は、 __ hash __ () は手を付けないまま、つまりスーパークラスの __ hash __ () メソッドが使われることになります (スーパークラスが object だった場合は、 id に基づいたハッシュ化にフォールバックするということになります)。

デフォルトだと、`eq`と`frozen`の値で`__hash__()`が生成されるかが決まるようですが、ミュータブルな状態では、`__hash__()`は生成されません。

```python:sample.py
@dataclass(eq=True, frozen=False)
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)

    print(user_status.__hash__())
    # => TypeError: 'NoneType' object is not callable
```

> __ hash __() があるということはそのクラスのインスタンスが不変 (イミュータブル) であることを意味します。

なので、下記のように、ミュータブルな状態で、`unsafe_hash=True`として、`__hash__()`を生成するのは、安全ではない使用方法です。

```python:sample.py
@dataclass(eq=True, frozen=False, unsafe_hash=True)
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)

    print(user_status.__hash__())
    # => 69849991143891
```

## frozen
`frozen=True`にすると、プロパティの書き換えができなくなります。値オブジェクトを作る際にクラスを**イミュータブル**にしたいなら、これを使うべきです。

```python:sample.py
@dataclass(frozen=True)
class UserStatus:
    username: str
    address: str
    age: int


if __name__ == "__main__":
    user_status = UserStatus("kota", "Nagoya", 23)

    user_status.address = "Tokyo"
    # => dataclasses.FrozenInstanceError: cannot assign to field 'address'
```


## おまけ
少しでも皆さんのお役に立てれば、幸いです。

[このアドベントカレンダー](https://qiita.com/advent-calendar/2018/renttle)内の内容で実装しているLINE Botです。ぜひ、使って遊んでみてください。
<a href="https://line.me/R/ti/p/%40qfx3178y"><img height="36" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>
計画・企画を多様な人から意見を聞けるサービス「[Renttle](https://renttle.jp)」を開発中です。ぜひ、使ってみて、レビューをください。