やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

PythonのDataClassについて調査する

 DataClassはクラス生成の糖衣構文である。

成果物

情報源

概要

 DataClassはクラス生成の糖衣構文である。__init__()を自動生成する。

0

from dataclasses import dataclass

@dataclass
class MyData:
    name: str
    age: int
    is_dead: bool
    def intro(self) -> str: return f'My name is {self.name}.'

 上記のコードから以下のコードを生成する。

class MyData:
    def __init__(self, name:str, age:int, is_dead:bool):
        self.name = name
        self.age = age
        self.is_dead = is_dead
    def intro(self) -> str: return f'My name is {self.name}.'

 以下のようにインスタンス生成する。

d = MyData('Yamada', 10, True)

1

 初期値を指定すれば引数を省略できる。

from dataclasses import dataclass

@dataclass
class MyData:
    name: str = ''
    age: int = 0
    is_dead: bool = False
    def intro(self) -> str: return f'My name is {self.name}.'

 上記のコードから以下のコードを生成する。

class MyData:
    def __init__(self, name:str='', age:int=0, is_dead:bool=False):
        self.name = name
        self.age = age
        self.is_dead = is_dead
    def intro(self) -> str: return f'My name is {self.name}.'

 以下のようにインスタンス生成する。

d = MyData()

ダンダーメソッド

 以下はすべて同じ。

from dataclasses import dataclass

@dataclass
class C: pass

@dataclass()
class C: pass

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C: pass
引数 概要
init Trueなら__init__メソッドを生成する。定義済みなら無視する。
repr Trueなら__repr__メソッドを生成する。定義済みなら無視する。
eq Trueなら__eq__メソッドを生成する。定義済みなら無視する。
order Trueなら__lt__,__le__,__gt__,__ge__メソッドを生成する。定義済みならTypeErrorを送出する。order=True,eq=FalseならValueErrorを送出する。
unsafe_hash Trueなら__hash__メソッドを生成しうる。eqfrozenが両方共Trueなら__hash__を生成する。eq=True, frozen=FalseならNoneになりハッシュ化不可能となる。eq=Trueならスーパークラス__hash__が使われる。
frozen Trueなら代入時に例外を送出する。

気になったこと

マングリングされない

from dataclasses import dataclass

@dataclass
class MyData:
    __private_prop: str = ''


if __name__ == "__main__":
    d = MyData()
    d.__private_prop = 'Yamada'
    print(d._MyData__private_prop)
    print(d.__private_prop)

 実行するとd._MyData__private_propは表示されず、d.__private_propは表示された。つまりマングリングされない。

Yamada

 これは「メンバ変数名のプレフィクスに__をつけるとマングリングされる」という従来のルールと矛盾する。

class MyData:
    def __init__(self):
        self.__private_prop = 'private!!'


d = MyData()
print(d._MyData__private_prop)
print(d.__private_prop) # AttributeError: 'MyData' object has no attribute '__private_prop'

 こちらは_クラス名__変数名のように_クラス名が付与される。これがマングリングである。だが、dataclassではこのマングリングがされない。

 つまり、dataclassで変数名のプレフィクス__をつけてもマングリングされない。それを覚えておく必要がありそうだ。

 ところで、従来のクラス生成でも、プレフィクス__をつけてマングリングされない変数を生成することはできる。以下のようにインスタンス生成後、新たなメンバを動的に追加することができてしまうからだ。

class MyData: pass


d = MyData()
d.__x = 'x'
print(d.__x) # OK
print(d._MyData__x) # AttributeError: 'MyData' object has no attribute '_MyData__x'

 dataclassはマングリングしない。プレフィクス__をつけてもそのままの名前で生成される。

コレクション型の初期化が面倒すぎる

 field(default_factory=list)のように書かねばならず冗長すぎる。

from dataclasses import dataclass, field

@dataclass
class MyData:
    l: list = field(default_factory=list)
    d: dict = field(default_factory=dict)
    t: tuple = field(default_factory=tuple)
    s: set = field(default_factory=set)

 もし以下のようにしたらエラーになってしまう。

from dataclasses import dataclass, field
@dataclass
class MyData:
    l: list = []
    d: dict = {}
    t: tuple = ()
    s: set = set()
ValueError: mutable default <class 'list'> for field l is not allowed: use default_factory

 もし以下のようにしたら単なるクラス変数になってしまう。

class MyData:
    l: list = []
    d: dict = {}
    t: tuple = ()
    s: set = set()

初期化後

 複雑な初期化をしたいときに用いる。field(init=False)することでインスタンス生成時の必須引数から除外できる。__post_init__という__init__に該当するメソッドにて初期化する式を書ける。

from dataclasses import dataclass, field
@dataclass
class MyData:
    name: str = field(init=False)
    def __post_init__(self):
        self.name = str(1+2+3)

d = MyData()
print(d.name)

初期化限定変数

 InitVarで型と初期値を定義できる。__post_init__()に渡される。

from dataclasses import dataclass, field, InitVar
@dataclass
class MyData:
    name: InitVar[str] = ''
    def __post_init__(self, name):
        self.name = str(1+2+3)


if __name__ == "__main__":
    d = MyData()
    print(d.name)

クラス変数

 クラス変数にしたいとき。(インスタンス変数でなく)

from dataclasses import dataclass, field, InitVar
from typing import ClassVar
@dataclass
class MyData:
    Name: ClassVar[str] = 'ClassVar-Name'


if __name__ == "__main__":
    print(MyData.Name)
    d = MyData()
    print(d.Name)

凍結したインスタンス

 代入したらエラーになる。

from dataclasses import dataclass
@dataclass(frozen=True)
class MyData:
    name: str = ''
    age: int = 0


if __name__ == "__main__":
    a = MyData()
    a.name = 'frozen=Trueにすると代入時にエラーを送出する。'
dataclasses.FrozenInstanceError: cannot assign to field 'name'

 存在しないプロパティ名を指定して、新たにプロパティを生成するべく代入しても同じFrozenInstanceErrorエラーとなる。

 その仕組みは__setattr__,__delattr__に例外を送出するようなメソッドを実装したから。

 そのため、以下のようにメンバ変数を削除しようとしても同様のエラーになる。

del a.name

 また、__init__で代入するときも同様のエラーになってしまう。そのため値を代入するにはobject.__setattr__を使わねばならない。

from dataclasses import dataclass, field, InitVar
@dataclass(frozen=True)
class MyData:
    name: InitVar[str] = ''
    age: int = 0
    def __post_init__(self, name):
#        self.name = name # dataclasses.FrozenInstanceError: cannot assign to field 'name'
        object.__setattr__(self, 'name', '初期値')


if __name__ == "__main__":
    a = MyData()
    print(a.name)

 生成後でもちゃんとエラーになってくれた。

a = MyData()
a.name = 'エラー' # dataclasses.FrozenInstanceError: cannot assign to field 'name'
object.__setattr__(a, 'name', '代入値') # dataclasses.FrozenInstanceError: cannot assign to field 'name'

継承できる

from dataclasses import dataclass, field, InitVar
@dataclass
class A:
    name: str = ''
@dataclass
class B(A):
    age: int = 0
@dataclass
class C(B):
    name: str = '山田'


if __name__ == "__main__":
    c = C()
    print(a.name)

 重複した名前はオーバーライドされる。つまり末端の子孫がもつフィールドが使われる。上記の例でいえばnameなら''でなく'山田'が使われる。

所感

 もっと簡単に書きたかった。とくにfield()だのInitVar, ClassVarだのそれらのimportだのdataclassデコレータだのが冗長すぎる。

 プライベート変数やsetter, getterも書きたかった。C#のクラス定義のほうがスマートだと思ってしまう。

 結局、Pythonもタイプセーフな方向に進んでいる。かつてPythonは「タイプ数が少ない」というメリットだったが、今や「モジュールなどの名前が短縮されてわかりにくい」というデメリットでしかなくなりつつある。なにせ型名を書いたり、セパレータとして:を書いたりするから余計に字数が増えている。その上、field()などを使わねばならない。ムリヤリ後付で実装したからこうなる。

 もっと簡単に書きたい。

対象環境

$ uname -a
Linux raspberrypi 5.4.83-v7l+ #1379 SMP Mon Dec 14 13:11:54 GMT 2020 armv7l GNU/Linux