あらすじ
『Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない』に触発されてドキュメントを読んでみたところ、面白そうな関数があったので紹介します。
以下のコードはこちらを参考にしてpython3.7環境で実行しています。
dataclasses
This module provides a decorator and functions for automatically adding generated special methods such as
__init__()and__repr__()to user-defined classes.
ユーザが定義するクラスに対して、特殊メソッドを自動生成してくれるデコレータ・関数のモジュールとのこと。
上記ドキュメントに関数が載っているほか、こちらとかこちらで日本語の解説がされています。
dataclassのコンソール実行例
>>> from dataclasses import dataclass
>>> @dataclass
... class Hoge:
... x: int
... y: str
... z: int = 0
...
>>> hoge = Hoge(x=100, y='hoge')
>>> hoge
Hoge(x=100, y='hoge', z=0)
>>>
>>> Hoge
<class '__main__.Hoge'>
Hogeクラスにdataclassデコレータをくっつけることで、__init__を定義しなくても変数x、y、zを初期化してくれています。
zのデフォルト引数も処理できていますね。
dataclasses.make_dataclassとは
dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
Creates a newdataclasswith namecls_name, fields as defined infields, base classes as given inbases, and initialized with a namespace as given innamespace.
fieldsis an iterable whose elements are each either name,(name, type), or(name, type, Field).
If just name is supplied,typing.Anyis used for type.
The values ofinit,repr,eq,order,unsafe_hash, andfrozenhave the same meaning as they do indataclass().
cls_nameに与えた名前のdataclassを作成する関数とのこと。とりあえず実行してみましょう。
>>> import dataclasses
>>> Fuga = dataclasses.make_dataclass(
... cls_name='Fuga',
... fields=[
... ('a', int),
... ('b', str),
... ('c', int, dataclasses.field(default=0))
... ]
... )
>>> fuga = Fuga(a=100, b='fuga')
>>> fuga
Fuga(a=100, b='fuga', c=0)
>>>
>>> Fuga
<class 'types.Fuga'>
make_dataclassに引数を渡すと、その引数の情報を元にしてFugaクラスを作ってくれました。
Fugaを評価してみるとtypes.Fugaになっており、types.new_class()によって生成されているようです。
types.new_class(name, bases=(), kwds=None, exec_body=None)
適切なメタクラスを使用して動的にクラスオブジェクトを生成します。
こう記述されているので、make_dataclassで生成されたFugaはクラスというよりはクラスオブジェクトと呼ぶべきかもしれません。
が、面倒なのでこれ以後もクラスオブジェクトのことをクラスと呼びます。ご留意ください。
make_dataclassを使った継承
継承するクラスをbasesに与えることで、そのクラスを継承できます。
上記のFugaクラスを継承したPiyoクラスをmake_dataclassで作ってみましょう。
>>> Piyo = dataclasses.make_dataclass(
... cls_name='Piyo',
... fields=[
... ('d', str, dataclasses.field(default='piyo'))
... ],
... bases=(Fuga,),
... )
>>> piyo = Piyo(a=-1, b='piyopiyo') # Fugaを継承しているので変数aとbが存在する
>>> piyo
Piyo(a=-1, b='piyopiyo', c=0, d='piyo')
>>>
>>> Piyo
<class 'types.Piyo'>
上記では引数basesにFugaクラスを指定することで、Fugaクラスを継承したPiyoクラスを記述しています。
piyoの初期化時にPiyoクラスのfieldsに存在しないaとbを指定してもエラーが発生しないことやpiyoの評価結果からFugaクラスを継承できていることがわかります。
basesの型としてtupleが指定されているため、多重継承も可能と思われます(まだ試してない)。
make_dataclassを使ったメソッド・クラス変数
namespaceにラムダ式を書くことでメソッドを、定数を書くことでクラス変数を与えることができます。
>>> C = dataclasses.make_dataclass(
... cls_name='C',
... fields=[
... ('n', int, dataclasses.field(default=0))
... ],
... namespace={
... 'n_lt': lambda self, x: self.n <= x, # インスタンスメソッド
... 'debug': lambda n: print(n), # クラスメソッド/スタティックメソッド的なものを書きたかった
... 'N': 100 # クラス変数
... }
... )
>>> c = C()
>>> c.n_lt(1)
True
>>> C.debug('test')
test
>>> c.debug('test')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes 1 positional argument but 2 were given
>>> C.N
100
>>> c.N
100
-
C.debug('test')は処理できていますが、c.debug('test')の実行が失敗していますね...。
エラーメッセージでは引数が多すぎると言われているので、おそらく内部的にc.debug(self, 'test')として処理されているのでしょう。
名前空間周りはまだ勉強していないのですが、クラスオブジェクト内の名前空間ではスタティックメソッドとして/クラスオブジェクトのインスタンス内の名前空間ではインスタンスメソッドとして処理されているのでしょうか? -
lambda式しか書けないので代入などを行う処理が書きにくいのが難点かと思います。
可読性の点から言っても、ちょっと込み入ったメソッドを書くなら素直に@dataclassデコレータをつけたクラスで書いた方がいいかな...。
結局、make_dataclass ←これいる?
This function is not strictly required, because any Python mechanism for creating a new class with
__annotations__can then apply thedataclass()function to convert that class to adataclass.
This function is provided as a convenience.
クラス定義時にアノテーションをつければそのクラスはdataclass()でdataclassに変換されるので、厳密に言えばいらないらしい。
実際にクラスを生成する処理はtypes.new_class()でやっているので当然っちゃ当然ですね。
あとがき
-
動的クラス生成が行えること自体は面白いと思うんですが、どうにも使い方が思い浮かばない。
リフレクションみたいにフレームワークのコード内で使われてたりするんでしょうか? -
クラス定義書いてインスタンス作ってメソッド呼んで、みたいな普通?の書き方に慣れていると動的クラス生成が出てきた時に頭が混乱しそうな気がします。
動的クラス生成がメジャーな手法になって、これがスタンダードな記法になりました!みんな知ってる!ってとこまでいかないとアンチパターン扱いされかねない。 -
dataclassesで初めて動的クラス生成が実装されたのかと思って書き始めましたが、調べてるうちにPython3.3でtypes.new_class()が実装されていたことを知りました。とほほ