2022/09/11

Pythonのdataclassが思いの外いい感じに思える今日この頃

Pythonのdataclassを活用する

Pythonでクラスを作成する際、最近はdataclassを用いる方法が主流になってきているようですので、Pythonを使う方は、どのような機能を持っているかを知っておくほうが良さそうです。

このdataclassが導入されたのは3.7からで、Python3.7以降を使っていらっしゃる方は、このdataclassを使うことができます。そしてPython3.10ではdataclassでslotsの有効・無効が設定できるようになりました。

dataclass導入まで、classは以下のように記述していました。


class Foo:
    def __init__(self, value: int) -> None:
        self.value = value

しかし、導入後は以下のように書くことができます。


from dataclasses import dataclass


@dataclass
class Foo:
    value: int

dataclassではいくつか設定できる項目があり、それぞれデフォルト値があります。例えば上記のように、初期化メソッドが必要なクラスの場合、デフォルトでinitがTrueになっているので、わざわざ__init__メソッドを書く必要がないですし、@dataclass(init=True)と書く必要もありません。__init__が要らない場合は、以下のように


@dataclass(init=False)
class Bar:
    value: int = 0

とすればいいだけです。

dataclassで設定できる項目をデフォルト値とともに明示的に書いてみると


@dataclass(
    init=True,
    repr=True,
    eq=True,
    order=True,
    unsafe_hash=False,
    frozen=False,
    match_args=True,
    kw_only=False,
    slots=False,
)
class Buzz:
    ...

となっています。この中で、私が「おっ」と思ったのはfrozenです。この項目をTrueにすると、フィールドへの代入ができなくなります。そしてデフォルトでTrueのeqも相まって、__hash__()も生成されます。つまり、このclassはイミュータブルな(変更不可)のclassにすることができます。


@dataclass(frozen=True)
class Person:
    name: str
    id: int

これを実際検証してみましょう。ついでにkw_onlyもTrueにしてみます。


>>> tom = Person(name="Tom", id=123456789)
>>> tom
Person(name='Tom', id=123456789)
>>> tom.id
123456789
>>> tom.id = 987654321
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'id'

このように、「idフィールドにはアサインすることはできない」というエラーメッセージが表示されます。関数型言語を割とよく使う私としてはなんとなく安心を与えてくれるものです。

サブパターンマッチにも使える

dataclassの項目には、上記のようにmatch_argsというものがあります。これは、Pythonで新しく取り入れられたパターンマッチの際に活躍するものです。例えば以下のようなclassがあるとします。


@dataclass
class UserAcount:
    name: str
    password: int

match_argsはデフォルトでTrueなので、特に明示的に書く必要はありません。使い方は以下のように新しく導入されたmatch~case文を使います。


>>> tom = UserAcount("Tom", 123)
>>> match tom:
...     case UserAcount("Tom", 123):
...         print("Correct")
...     case _:
...         print("Incorrect")
...
Correct

単純な例ですが、今までのようなif文を使ったものよりも簡潔に書くことができるケースが増えていくように思います。

まとめ

このところ、Pythonをあまり書くことがなかったのですが、速度も上がり、様々な機能が追加され、以前よりもいい感じになっているのではないかいう印象を受けました。ダンダー(Pythonではアンダースコア一つをサンダー、ダブルアンダースコアをダンダーとよく言う)でおなじみの特殊メソッド書く手間が省けて見た目もすっきり感がアップしました。また使う機会があれば使っていこうかなんて思っています(今はOCamlとRustへの興味のほうが勝っていますが)