Edited at

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

このアドベントカレンダーでも何度か登場している 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 __()の記述なしでインスタンス化できる


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に変更する


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プロパティを呼ぶとどうなるだろう。


sample.py

print(user_status.age)

# AttributeError: 'UserStatus' object has no attribute 'age'

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

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


repr

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


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

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


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に変更する


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と出力されます。

「公式ドキュメントのデータモデル」には、このように書かれています。


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


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


order

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


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


上記と同じ理由で、インスタンス同士の比較はうまくできません。そこで、order=Trueにしてみます。


デフォルト値をTrueに変更する


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


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


unsafe_hash


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


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


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__()を生成するのは、安全ではない使用方法です。


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にすると、プロパティの書き換えができなくなります。値オブジェクトを作る際にクラスをイミュータブルにしたいなら、これを使うべきです。


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'



おまけ

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

このアドベントカレンダー内の内容で実装しているLINE Botです。ぜひ、使って遊んでみてください。



計画・企画を多様な人から意見を聞けるサービス「Renttle」を開発中です。ぜひ、使ってみて、レビューをください。