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がメタクラスであると納得する理由を以下のように説明できます。

  1. typeのインスタンス化した結果、生成されるものはクラスである
  2. インスタンス化した結果生成されたものがクラスであるものはメタクラスである
  3. よって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')が出る

一つ注意点があって、このメタクラスはインスタンス変数名を略すことができますが、クラス変数については、見つけてくれません。

以下個人的に面白いと思ったコードを紹介します。

まとめ

Pythonのmetaclassは非常に強力な機能で思いもよらない便利なプログラムを作ることが可能です。