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__メソッドを生成しうる。eq とfrozen が両方共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()
などを使わねばならない。ムリヤリ後付で実装したからこうなる。
もっと簡単に書きたい。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
$ uname -a Linux raspberrypi 5.4.83-v7l+ #1379 SMP Mon Dec 14 13:11:54 GMT 2020 armv7l GNU/Linux