【Python 雑談・雑学】メタクラス ( metaclass ) とデコレータ ( decorator ) で遊んでみる、考えてみる! 投稿一覧へ戻る

Tags: Python , miscellaneous , decorator , __new__ , metaclass

Published 2020年7月21日20:29 by T.Tsuyoshi

クラスはオブジェクトの設計図であり、メタクラス ( metaclass ) はクラスの設計図である、といわれたところで全然分かりません。


ただ、昨今のプログラミング環境においてブラックボックス的に機能を提供してくれる先進的なフレームワークやライブラリ、例えば Django における Forms など、が、このメタクラスによって実現されていることは確かなんです。


どんなものなのか、を理解するのは非常に大変ですが、どんなことができるのか、を見ることによって何らかのヒントになるかもしれない、というのが今回の記事の目的です。


今回取り上げている例では、あまり複雑ではないことをわざわざデコレータ ( decorator ) とメタクラスを共に使って実現しています。
そして、わざと回りくどく実装している部分もあります ( ちょっとした興味をそそるために... )。


def calc_converter(ope):
def make_hook(f):
f.is_convert = ope
return f
return make_hook


class MyType(type):
def __new__(mcls, name, bases, attrs):
print(MyType.__dict__)
newattrs = {}
for attrname, attrvalue in attrs.items():
if getattr(attrvalue, 'is_convert', 0):
func_name = f"__{getattr(attrvalue, 'is_convert')}__"
newattrs[func_name] = getattr(mcls, func_name)
else:
newattrs[attrname] = attrvalue
return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

def __add__(self, other):
return self.value + other.value

def __sub__(self, other):
return self.value - other.value


class Example(metaclass=MyType):
def __init__(self, value):
self.value = value

@calc_converter('add')
def myfunction1(self, other):
pass

@calc_converter('sub')
def myfunction2(self, other):
pass

def myfunction3(self, other):
pass


a = Example(10)
b = Example(5)

print(a + b)
print(a - b)



個別のパーツを見る前にこのモジュール全体の流れを押さえておいてください。


このモジュールには、関数定義、クラス定義、実行ステートメントが含まれています。
Python は上から順番に処理をしていき、関数定義、クラス定義に関してはそれぞれの名前空間を確保し、属性を保存します。


このとき本体は実行しませんが、実行文 ( print ステートメント等 ) が含まれていれば実行します。


クラス定義に関してもう少し詳しく見ていきましょう。


クラスはデフォルトでは type() を使って構築されますが、使用すべきメタクラスが指定されている場合はそのメタクラスが使われます。


まずはメタクラスによってクラス用の名前空間が用意され、そこにはそのクラスの属性辞書が保存されます。


もし属性がデコレータでラップされている場合は、そのデコレータが適用された結果が属性値として保存されます。


そして最終的なクラスが構築されますが、メタクラスに __new__() が定義されている場合、この特殊関数の返り値がそのクラスの最終形になる、というわけです。インスタンス化して使っているのはこの最終形のクラスです。


分かったような、分からないような、なかなかのワクワクドキドキです!?


ではデコレータから見ていきましょう。


def calc_converter(ope):
def make_hook(f):
f.is_convert = ope
return f
return make_hook



このデコレータでは、修飾された関数の属性として is_convert を追加し、その属性の値としてデコレータの引数の値を設定しています。


Python では関数もオブジェクトであることを思い出してください。属性辞書をもっていますから、それに追加しています。


続いてメタクラス本体です。


class MyType(type):
def __new__(mcls, name, bases, attrs):
print(MyType.__dict__)
newattrs = {}
for attrname, attrvalue in attrs.items():
if getattr(attrvalue, 'is_convert', 0): # 1:
func_name = f"__{getattr(attrvalue, 'is_convert')}__" # 2:
newattrs[func_name] = getattr(mcls, func_name) # 3:
else:
newattrs[attrname] = attrvalue
return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

def __add__(self, other):
return self.value + other.value

def __sub__(self, other):
return self.value - other.value



今回「通常」のクラスとして定義した Exmaple クラスは、メタクラスとして MyType クラスを使ってね、と指定しています。


ですから、Example クラスの属性辞書を用意しているのはこの MyType メタクラスで、Example クラスの最終形は MyType クラスの __new__() メソッドの返却値、ということになります。


__new__() に渡されてきている attrs パラメータが Example クラスのデフォルトの属性辞書です。


内容を覗いてみると...
{'__module__': '__main__',
'__qualname__': 'Example',
'__init__': <function Example.__init__ at 0x00000000026E6040>,
'myfunction1': <function Example.myfunction1 at 0x00000000026E6160>,
'myfunction2': <function Example.myfunction2 at 0x00000000026E61F0>,
'myfunction3': <function Example.myfunction3 at 0x00000000026860D0>}



そして myfunction1、myfunction2、myfunction3 の実体の属性を覗いてみると...


attrs['myfunction1'].__dict__ の内容:
{'is_convert': 'add'}

attrs['myfunction2'].__dict__ の内容:
{'is_convert': 'sub'}

attrs['myfunction3'].__dict__ の内容:
{}



calc_converter デコレータでラップされたメソッドの属性には 'is_convert' が追加されているのが分かります。


おまけに MyType メタクラス自身の属性辞書の内容は...
{'__module__': '__main__',
'__new__': <staticmethod object at 0x0000000002447970>,
'__add__': <function MyType.__add__ at 0x0000000002692F70>,
'__sub__': <function MyType.__sub__ at 0x0000000002696040>,
'__doc__': None}



そしてメタクラス MyType の __new__() では、

1: 属性値が 'is_convert' 属性を含んでいる ( 例: {'is_convert': 'add'} )

2: 'is_convert' 属性の値を '__' ( ダブルアンダースコア ) で囲んだ「属性名」を作成する ( 例: '__add__' )

3: MyType メタクラスの属性辞書から 2: で作成した属性名と一致する値を取り出して、属性辞書に追加する
( 例: newattrs['__add__'] = <function MyType.__add__ at 0x0000000002692F70> )



という処理を行っています。


そして最終的に、上記のように変更を加えた属性辞書を持つ「変身した」Example クラスが定義される、ということになります。


作り変えられた後の Example クラスの属性辞書の内容です...
{'__module__': '__main__',
'__qualname__': 'Example',
'__init__': <function Example.__init__ at 0x00000000026D8040>,
'__add__': <function MyType.__add__ at 0x00000000026D2EE0>,
'__sub__': <function MyType.__sub__ at 0x00000000026D2F70>,
'myfunction3': <function Example.myfunction3 at 0x00000000026D80D0>}



'myfunction1' と 'myfunction2' が '__add__' と '__sub__' にそれぞれ置き換えられ、それぞれの属性値である関数も、MyType メタクラス内で定義されている関数に置き換わっているのが分かると思います。


この結果、元の Example クラスでは提供していない __add__() や __sub__() といった特殊関数を提供可能になり、Example オブジェクト同士の + (足し算) や - (引き算) がサポートできるようになります。


class Example(metaclass=MyType):
def __init__(self, value):
self.value = value

@calc_converter('add')
def myfunction1(self, other):
pass

@calc_converter('sub')
def myfunction2(self, other):
pass

def myfunction3(self, other):
pass


a = Example(10)
b = Example(5)


print(a + b)
# 15

print(a - b)
# 5



最後に、もし Example クラスが通常のメタクラス type() を経由して作成された場合を見てみましょう。


class Example:
def __init__(self, value):
self.value = value

@calc_converter('add')
def myfunction1(self, other):
pass

@calc_converter('sub')
def myfunction2(self, other):
pass

def myfunction3(self, other):
pass


a = Example(10)
b = Example(5)


print(a + b)
# Traceback...
# TypeError: unsupported operand type(s) for +: 'Example' and 'Example'



メタクラス、どうですか?


大きな可能性を感じるような、特に必要ないような、何とも中途半端な感じでしょうか?


少しでも興味が湧いたら自分なりに色々と試してみて下さい。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)