Python3.7のdataclassesでクラスを動的に生成してみる

# あらすじ
[『Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない』](https://qiita.com/tag1216/items/13b032348c893667862a)に触発されてドキュメントを読んでみたところ、面白そうな関数があったので紹介します。
以下のコードは[こちら](https://qiita.com/tag1216/items/949befbbf6d4030ab0b3)を参考にして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. 

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

ユーザが定義するクラスに対して、[特殊メソッド](https://docs.python.org/ja/3.7/glossary.html#term-special-method)を自動生成してくれるデコレータ・関数のモジュールとのこと。
上記ドキュメントに関数が載っているほか、[こちら](https://qiita.com/tag1216/items/13b032348c893667862a)とか[こちら](https://qiita.com/massa142/items/6dbfeb88092dea4f95d8)で日本語の解説がされています。


## dataclassのコンソール実行例

```python:とりあえず使ってみる
>>> 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 new `dataclass` with name `cls_name`, fields as defined in `fields`, base classes as given in `bases`, and initialized with a namespace as given in `namespace`.
`fields` is an iterable whose elements are each either name, `(name, type)`, or `(name, type, Field)`.
If just name is supplied, `typing.Any` is used for type.
The values of `init`, `repr`, `eq`, `order`, `unsafe_hash`, and `frozen` have the same meaning as they do in `dataclass()`.

`cls_name`に与えた名前のdataclassを作成する関数とのこと。とりあえず実行してみましょう。

```python:動的にクラスを生成
>>> 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)`
適切なメタクラスを使用して動的にクラスオブジェクトを生成します。

https://docs.python.org/ja/3/library/types.html#types.new_class


こう記述されているので、`make_dataclass`で生成された`Fuga`はクラスというよりはクラスオブジェクトと呼ぶべきかもしれません。
が、面倒なのでこれ以後もクラスオブジェクトのことをクラスと呼びます。ご留意ください。


## make_dataclassを使った継承

継承するクラスを`bases`に与えることで、そのクラスを継承できます。
上記の`Fuga`クラスを継承した`Piyo`クラスを`make_dataclass`で作ってみましょう。

```python:動的に継承
>>> 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`にラムダ式を書くことでメソッドを、定数を書くことでクラス変数を与えることができます。

```python:メソッドのあるクラスを動的に生成
>>> 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 the `dataclass()` function to convert that class to a `dataclass`.
This function is provided as a convenience.

クラス定義時にアノテーションをつければそのクラスは`dataclass()`で`dataclass`に変換されるので、厳密に言えばいらないらしい。
実際にクラスを生成する処理は`types.new_class()`でやっているので当然っちゃ当然ですね。


# あとがき
- 動的クラス生成が行えること自体は面白いと思うんですが、どうにも使い方が思い浮かばない。
[リフレクション](https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%95%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3_(%E6%83%85%E5%A0%B1%E5%B7%A5%E5%AD%A6))みたいにフレームワークのコード内で使われてたりするんでしょうか？

- クラス定義書いてインスタンス作ってメソッド呼んで、みたいな普通？の書き方に慣れていると動的クラス生成が出てきた時に頭が混乱しそうな気がします。
動的クラス生成がメジャーな手法になって、これがスタンダードな記法になりました！みんな知ってる！ってとこまでいかないとアンチパターン扱いされかねない。

- `dataclasses`で初めて動的クラス生成が実装されたのかと思って書き始めましたが、調べてるうちにPython3.3で`types.new_class()`が実装されていたことを知りました。とほほ