Pythonのメタプログラミングについてのまとめと活用例
Pythonについて勉強するうちにメタプログラミングについて、興味が出てきたのでまとめます。
メタプログラミングとは
メタプログラミングについてWikipediaを参照すると以下のように説明されています。
メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法
また、より簡潔に、「プログラムのプログラミング」と説明することができます。
Pythonにおけるメタプログラミング
Pythonでは、メタプログラミングとは、一般的に以下の概念を利用したプログラミングについて指します。
- metaclass
- decorator
本記事ではmetaclassについて説明します。
metaclass
metaclassとは
クラスをインスタンス化するとオブジェクトが生成されます。この考え方を拡張しメタクラスについては、”インスタンス化するとクラスが生成されるクラス”と説明できます。
つまり、Pythonのインスタンス生成は以下のように2つあるということができます。
- metaclass -> class
- class -> object
以下の記事がより詳しく書かれているので、参照してください。
metaclassでなにができるのか
metaclassを使用すると、オブジェクトの生成プロセスをコントロールすることができます。通常のプログラムでは扱えない、抽象的なレベルでコーディング出来ます。具体例はPython言語リファレンスのデータモデルから以下のように引用できます。
メタクラスは限りない潜在的利用価値を持っています。これまで試されてきたアイデアには、ログ記録、インタフェースのチェック、自動デリゲーション、自動プロパティ生成、プロキシ、フレームワーク、そして自動リソースロック/同期といったものがあります。
メタクラス実装の単純な例
詳細な説明の前に単純な例を見てもらったほうが早いかと思います。
# 最も単純な例
class A(type):
pass
# __new__関数を使用した例
class B(type):
def __new__(cls, name, bases, dict):
dict['foo'] = 'bar'
return type.__new__(cls, name, bases, dict)
上記コードを解説していきます。まず、いきなりtype
というよくわからないものを継承していて面食らったと思いますが、これについてはあとで説明していきます。
クラスB
ですが、これは変数名がfoo
で内容が"bar"
のメンバ変数をメタクラスを適用させるクラスに追加しています。
次にメタクラスの使用方法ですが、メタ操作を行いたいクラスにおいて以下のように使います。
class C(metaclass=B):
pass
これにより、クラスC
にはメンバ変数foo
が自動的に追加されます。
typeとはなにか
Pythonには、インスタンスからその型を取り出す関数としてtype
がありますが、type
関数にはもうひとつ、 第1引数に文字列でクラス名、第2引数に親クラスの列、第3引数にクラスのメソッドや属性を定義した dict を渡して type を呼び出すとクラスを動的に定義することができる機能があります。たとえば、クラス名がAで、クラスX, Yを継承しており、クラス変数として、変数名がnameで内容がtaroskyであるものは以下のように生成します。
class X: pass
class Y: pass
a = type('A', (X, Y), {'name': 'tarosky'})
このコードは以下のコードとは本質的にはほぼ等価です。
class X: pass
class Y: pass
class A(X, Y):
name = 'tarosky'
a = A
ここでtype
はクラスの情報をコンストラクタで受けとり、メタクラスのインスタンス(=クラス)を生成しているとも考えることができます。つまり、type
がメタクラスであると納得する理由を以下のように説明できます。
type
のインスタンス化した結果、生成されるものはクラスである- インスタンス化した結果生成されたものがクラスであるものはメタクラスである
- よって
type
はメタクラスである
またこの理由より、メタクラスを実装する際にtypeを継承することでメタクラスが宣言できる理由がわかると思います。
__new__
関数
インスタンスができるプロセスを説明すると最初に__new__
関数が呼ばれ、そして__new__
は未初期化のインスタンスを生成します。そこから、__init__
関数が呼び出されインスタンスを初期化します。また混同しやすい点なのですが、metaclassの__new__
関数とclassの__new__
関数は全く別物です。例えばメタクラスを適用したクラスを継承したクラスに__new__
関数を実装したとしてもメタクラスの__new__
はオーバーロードできません。これらの特殊関数についてまとめると以下の通りになります。
__new__
- 未初期化のインスタンスを生成する。
- 継承などの動作も扱うことが可能
__init__
- インスタンスを初期化する
活用例
Pythonの特徴的な機能の一つに多重継承があります。多重継承はコードが複雑になるという理由やリスコフの置換原則を守っていない等の理由で他の言語(JavaやRuby)だと禁止されていたりします。そこで唐突なのですが、Pythonを使いたいけど、多重継承を禁止した上でコーディングがしたいというニーズのために今回は練習を兼ねて、多重継承を禁止するコードを書いてみたいと思います。
class BanningMultipleInheritance(type):
def __new__(cls, name, bases, dict):
if len(bases) >= 2:
raise TypeError('多重継承はダメ')
return type.__new__(cls, name, bases, dict)
class A(metaclass=BanningMultipleInheritance):
pass
class B:
pass
if __name__ == '__main__':
type('aaa', (A, B), {}) # TypeErrorが出る
このように、メタプログラミングを用いると多重継承を禁止するコードがいとも簡単に書けてしまいます。
次に、インスタンス変数を略しても、(例: name
のところをn
)変数を見つけてくれるようにするメタクラスを書いてみます。
import re
class SearchAttr(type):
def __new__(cls, name, bases, dict):
def search_attr(self, name):
attrs = [k for k in self.__dict__.keys()
if re.match(r'^{0}.*$'.format(name), k)]
if attrs == []:
raise AttributeError('Not Found')
elif len(attrs) != 1:
raise AttributeError('Not unique')
else:
return self.__dict__[attrs[0]]
dict['__getattr__'] = search_attr
return type.__new__(cls, name, bases, dict)
class A(metaclass=SearchAttr):
def __init__(self):
self.name = 'tarosky'
self.sex = 'male'
self.score = 120
if __name__ == '__main__':
a = A()
print(a.name) # tarosky
print(a.n) # tarosky
print(a.s) # AttributeError('Not unique')が出る
一つ注意点があって、このメタクラスはインスタンス変数名を略すことができますが、クラス変数については、見つけてくれません。
以下個人的に面白いと思ったコードを紹介します。
- auto_property
get_x
というメソッドを定義するとプロパティx
が自動的に生成される
- Pythonによる黒魔術入門
- LoggingHookKlass
- メンバ関数が呼び出されると、print文を実行する
- LoggingHookKlass
- Python 3 Patterns, Recipes and Idioms – Metaprogramming
- final
- 継承を禁止する Javaで言う
final
修飾子
- 継承を禁止する Javaで言う
- Singleton
- GoFデザインパターンのSingletonのmetaclassによる実装
- final
まとめ
Pythonのmetaclassは非常に強力な機能で思いもよらない便利なプログラムを作ることが可能です。