Python
python3

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

Data Classes とは

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

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

基本的な使い方

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

@dataclasses.dataclass
class User:
    name: str
    age: int = 0
クラスを使う
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

カスタマイズ

メソッドを定義する

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

import dataclasses

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

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

イミュータブルにする

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

@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'

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

@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=[])

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

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

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

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

@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__の後に呼ばれます。

@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で定義したクラス変数はフィールドとは認識されずインスタンスには保持されません。

@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)

ユーティリティ関数

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

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

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

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

tupleに変換する

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

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

最後に

基本的な使い方だけだと namedtuple と大きな違いはないのですが、Data Classes では色々とカスタマイズができることがわかりました。
今後クラス定義は Data Classes を使うのが標準的になりそうな予感がします。