検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>

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

Published 2020年7月21日20:29 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

クラスはオブジェクトの設計図であり、メタクラス ( 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'



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


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


少しでも興味が湧いたら自分なりに色々と試してみて下さい。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】今回も懲りずにメタクラス ( metaclass ) - __init_subclass__() 特殊関数でメタクラスをもっと活用しよう!
【 Effective Python, 2nd Edition 】サブクラス定義に付随させて必ず行いたい操作がある場合は、メタクラス ( metaclass )、または、__init_subclass__ 特殊関数を利用してド忘れを防止しよう!
【Python 雑談・雑学】 デコレータ (decorators) を理解しよう - デコレータ、オリジナル関数からの引数の渡し方、受け取り方
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!
【 Effective Python, 2nd Edition 】クラス作成時の setter メソッド、getter メソッドの利用は最小限に。可能な限り public 属性によるインターフェース構築を目指しましょう
Practical Python Design Patterns - Python で学ぶデザインパターン: The Prototype Pattern - Part. 1 「ちょっとゲームのことを考えてみよう」の巻
【Python 雑談・雑学】 list comprehension (リスト内包表記) は節度を持って使ってください、という話