11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-24

あらすじ

『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__を定義しなくても変数xyzを初期化してくれています。
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を作成する関数とのこと。とりあえず実行してみましょう。

動的にクラスを生成
>>> 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'>

上記では引数basesFugaクラスを指定することで、Fugaクラスを継承したPiyoクラスを記述しています。
piyoの初期化時にPiyoクラスのfieldsに存在しないabを指定してもエラーが発生しないことや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 the dataclass() function to convert that class to a dataclass.
This function is provided as a convenience.

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

あとがき

  • 動的クラス生成が行えること自体は面白いと思うんですが、どうにも使い方が思い浮かばない。
    リフレクションみたいにフレームワークのコード内で使われてたりするんでしょうか?

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

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

11
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?