Pythonのメタクラスとメタプログラミング

もし、あなたの代わりにコードを書いてくれるコンピュータ・プログラムがあったらと想像してください。それは可能ですが、機械があなたのためにすべてのコードを書いてくれるわけではありません。

このテクニックはメタプログラミングと呼ばれ、コードフレームワークの開発者に人気があります。Ruby On RailsやTensorFlowのような多くの人気フレームワークやライブラリで、コード生成やスマートな機能を実現しているのは、この方法によるものです。

Elixir、Clojure、Rubyなどの関数型プログラミング言語は、そのメタプログラミングの機能で注目されています。このガイドでは、Pythonでどのようにメタプログラミングの力を利用できるかを紹介します。コード例はPython 3用に書かれていますが、いくつかの調整でPython 2でも動作します。

Pythonのメタクラスとは?

Pythonはオブジェクト指向言語であり、クラスを簡単に扱うことができます。

Pythonのメタプログラミングは、メタクラスと呼ばれる特別な新しいタイプのクラスに依存しています。このタイプのクラスは、簡単に言うと、他のコードが実行されるときに行わせたい舞台裏のコード生成に関する指示を保持します。

Wikipediaには、メタクラスについてよくまとまっています。

とあります。
オブジェクト指向プログラミングにおいて、メタクラスは、インスタンスがクラスであるクラスである。
となります。
>
>

クラスを定義すると、そのクラスのオブジェクトは、クラスを設計図として作成されます。

しかし、クラスそのものはどうでしょうか。クラス自体の設計図はどうなっているのでしょうか?

そこで登場するのが、メタクラスです。クラスがそのクラスのインスタンスの設計図であるように、メタクラスはクラスそのものの設計図です。メタクラスは、他のクラスのプロパティを定義するクラスです。

メタクラスでは、コード内で定義される新しいクラスに追加すべきプロパティを定義することができます。

例えば、次のメタクラスのコードサンプルでは、このメタクラスをテンプレートとして使用する各クラスに hello プロパティを追加しています。これは、このメタクラスのインスタンスである新しいクラスは、自分で定義しなくても hello プロパティを持つことを意味します。

# hello_metaclass.py
# A simple metaclass
# This metaclass adds a 'hello' method to classes that use the metaclass
# meaning, those classes get a 'hello' method with no extra effort
# the metaclass takes care of the code generation for us
class HelloMeta(type):
    # A hello method
    def hello(cls):
        print("greetings from %s, a HelloMeta type class" % (type(cls())))


# Call the metaclass
    def __call__(self, *args, **kwargs):
        # create the new class as normal
        cls = type.__call__(self, *args)


# define a new hello method for each of these classes
        setattr(cls, "hello", self.hello)


# return the class
        return cls


# Try out the metaclass
class TryHello(object, metaclass=HelloMeta):
    def greet(self):
        self.hello()


# Create an instance of the metaclass. It should automatically have a hello method
# even though one is not defined manually in the class
# in other words, it is added for us by the metaclass
greeter = TryHello()
greeter.greet()


このコードを実行した結果、新しい TryHello クラスは、次のような挨拶を出力することができました。

greetings from <class '__main__.tryhello'="">, a HelloMeta type class


この印刷を担当するメソッドは、クラスの宣言の中では宣言されていません。むしろ、メタクラス (この場合 HelloMeta) が実行時にコードを生成し、自動的にそのメソッドをクラスに付加しています。

このコードをPythonのコンソールにコピー&ペーストして、実際に動作しているところをご覧ください。また、コードの各部分で何を行ったか理解するために、コメントを読んでください。新しいオブジェクトは greeter という名前で、 TryHello クラスのインスタンスとして生成されています。しかし、 TryHello クラスの宣言ではそのようなメソッドは定義されていないのに、 TryHelloself.hello メソッドを呼び出すことが出来ています。

存在しないメソッドを呼び出してエラーになるのではなく、 HelloMeta クラスをメタクラスとして使用しているため、 TryHello には自動的にそのようなメソッドが付与されるのです。

メタクラスは、データだけでなく、他のコードも変換するようなコードを書く能力を与えてくれます。例えば、クラスがインスタンス化されるときに変換することができます。上の例では、私たちのメタクラスは、私たちのメタクラスをそのメタクラスとして使用するように定義した新しいクラスに、新しいメソッドを自動的に追加しています。

これはメタプログラミングの一例です。メタプログラミングとは、メタクラスと関連する技術を使って、バックグラウンドで何らかの形でコード変換を行うコードを書くことです。

メタプログラミングの美しいところは、ソースコードを出力するのではなく、そのコードの実行結果だけを返してくれるところです。私たちのプログラムのエンドユーザーは、バックグラウンドで起こっている「魔法」に気づかないのです。

プログラマーとして、より少ないコードしか書かなくて済むように、バックグラウンドでコード生成を行うソフトウェアフレームワークについて考えてみてください。ここにいくつかの素晴らしい例があります。

  • Django
  • SQLAlchemy
  • Flask
  • Theano

Python以外では、Ruby On Rails(Ruby)やBoost(C++)などの人気のライブラリは、メタプログラミングがフレームワーク作者によってコードを生成し、バックグラウンドで面倒を見るために使われている例です。

その結果、フレームワークでコーディングするプログラマの多くの作業を自動化し、シンプルなエンドユーザー用APIを実現します。

フレームワークのソースコードに組み込まれた多くのメタプログラミングは、そのシンプルさを舞台裏で機能させることを引き受けているのです。

理論編 メタクラスがどのように機能するかを理解する

Pythonのメタクラスがどのように動作するかを理解するためには、Pythonの型の概念に非常に慣れている必要があります。

型とは、Pythonのオブジェクトのデータまたはオブジェクトの命名法です。

オブジェクトの型を見つける

Python REPL を使って、次のように簡単な文字列オブジェクトを作成し、その型を調べてみましょう。

&gt;&gt;&gt; day = "Sunday"
&gt;&gt;&gt; print("The type of variable day is %s" % (type(day)))
The type of variable day is <type 'str'="">


予想通り、変数 day は文字列の型である str であると出力されます。オブジェクトの型は、組み込みの type 関数にオブジェクトの引数を1つ与えるだけで調べることができます。

クラスの型を見つける

では、 "Sunday""hello" のような文字列は str 型ですが、 str 自身はどうでしょうか?クラス str の型は何でしょうか?

もう一度、Pythonのコンソールで入力してみてください。

&gt;&gt;&gt; type(str)
<type 'type'="">


今度は、strtype という型であると出力されます。

タイプ・タイプ・オブ・タイプ

しかし、type 自身はどうなのでしょうか?type` の型は何なのでしょうか?

&gt;&gt;&gt; type(type)
<type 'type'="">


結果は、またしても「タイプ」です。このように、 typeint のようなクラスのメタクラスであるだけでなく、それ自身のメタクラスでもあるのです!このメタクラスは、 int のようなクラスのメタクラスであると同時に、 type 自身のメタクラスでもあるのです。

メタクラスで使用される特殊なメソッド

この時点で、理論を少し復習しておくとよいでしょう。メタクラスはインスタンスがそれ自身クラスであり、単なるオブジェクトではないことを忘れないでください。

Python 3 では、新しいクラスの定義に意図したマスタークラスを渡すことで、新しいクラスを作成する際にメタクラスを割り当てることができます。

Python のデフォルトのメタクラスである type 型は、新しいメタクラスがユニークなコード生成動作を実装するためにオーバーライドできる特別なメソッドを定義しています。以下は、メタクラスに存在するこれらの「魔法の」メソッドの簡単な概要です。

  • __new__: このメソッドは、メタクラスをベースとしたクラスのインスタンスが生成される前にメタクラス上で呼び出されます。
  • __init__: このメソッドは、メタクラスをベースにしたクラスのインスタンスが作成される前に、メタクラスから呼び出されます。このメソッドは、インスタンス/オブジェクトが作成された後に値を設定するために呼び出されます。
  • __prepare__: インスタンス/オブジェクトが作成された後に値を設定するために呼び出されるメソッドです。属性を格納するマッピングにクラスの名前空間を定義する。
  • __call__: クラスの名前空間を定義する。このメソッドは、新しいクラスのコンストラクタを使用してオブジェクトを作成する際に呼び出されます。

これらは、デフォルトのメタクラスである type と異なる動作をさせるために、カスタムのメタクラスでオーバーライドするメソッドです。

メタプログラミングの実践1:デコレーターを使って関数の振る舞いを変換する

メタクラスを使ってメタプログラミングの練習をする前に、一歩引いてみましょう。Pythonのメタプログラミングの一般的な使い方は、デコレータを使うことです。

デコレータとは、関数の実行を変換する関数です。つまり、ある関数を入力として受け取り、別の関数を返すというものです。

例えば、任意の関数を受け取り、その関数の名前を出力してから元の関数を実行するデコレータがある。これは、たとえば関数の呼び出しを記録するのに便利です。

# decorators.py


from functools import wraps


# Create a new decorator named notifyfunc
def notifyfunc(fn):
    """prints out the function name before executing it"""
    @wraps(fn)
    def composite(*args, **kwargs):
        print("Executing '%s'" % fn.__name__)
        # Run the original function and return the result, if any
        rt = fn(*args, **kwargs)
        return rt
    # Return our composite function
    return composite


# Apply our decorator to a normal function that prints out the result of multiplying its arguments
@notifyfunc
def multiply(a, b):
    product = a * b
    return product


このコードをコピーしてPython REPLに貼り付けることができます。デコレータを使うと、入力関数の代わりに合成関数が実行されるのがよいところです。上記のコードの結果、乗算関数は計算が実行される前に実行中であることを通知します。

&gt;&gt;&gt; multiply(5, 6)
Executing 'multiply'
30
&gt;&gt;&gt;
&gt;&gt;&gt; multiply(89, 5)
Executing 'multiply'
445


つまり、デコレータはメタクラスと同じようにコードを変換することができますが、よりシンプルなものなのです。デコレータを使いたいのは、コードの周りに一般的なメタプログラミングを適用する必要がある場合でしょう。たとえば、すべてのデータベースコールをログに記録するデコレータを書くことができます。

メタプログラミングの実践2:メタクラスをデコレーター関数のように使う

メタクラスは、クラスの属性を置き換えたり、変更したりすることができます。新しいオブジェクトが作成される前や、新しいオブジェクトが作成された後にフックする力を持っています。その結果、メタクラスを使用する目的に関して、より大きな柔軟性を得ることができます。

以下では、先ほどのデコレータと同じ結果を得るためのメタクラスを作成します。

この2つを比較するために、両方の例を並べて実行し、注釈付きのソースコードを追って見てください。なお、REPL がコードの書式を保持する場合は、コードをコピーしてそのまま REPL に貼り付けることができます。

# metaclassdecorator.py
import types


# Function that prints the name of a passed in function, and returns a new function
# encapsulating the behavior of the original function
def notify(fn, *args, **kwargs):


def fncomposite(*args, **kwargs):
        # Normal notify functionality
        print("running %s" % fn.__name__)
        rt = fn(*args, **kwargs)
        return rt
    # Return the composite function
    return fncomposite


# A metaclass that replaces methods of its classes
# with new methods 'enhanced' by the behavior of the composite function transformer
class Notifies(type):


def __new__(cls, name, bases, attr):
        # Replace each function with
        # a print statement of the function name
        # followed by running the computation with the provided args and returning the computation result
        for name, value in attr.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                attr[name] = notify(value)


return super(Notifies, cls).__new__(cls, name, bases, attr)


# Test the metaclass
class Math(metaclass=Notifies):
    def multiply(a, b):
        product = a * b
        print(product)
        return product


Math.multiply(5, 6)


# Running multiply():
# 30


class Shouter(metaclass=Notifies):
    def intro(self):
        print("I shout!")


s = Shouter()
s.intro()


# Running intro():
# I shout!


メタクラス Notifies を使用しているクラス、例えば ShouterMath では、生成時にそのメソッドが拡張バージョンに置き換えられ、最初に print 文で現在実行中のメソッドの名前が通知されるようになりました。これは、以前デコレータ関数を使用して実装した動作と同じです。

メタクラスの例1:サブクラス化できないクラスの実装

メタプログラミングの一般的な使用例には、クラスインスタンスの制御が含まれます。

例えば、多くのコードライブラリでシングルトンが使用されています。シングルトンクラスはインスタンスの生成を制御し、プログラム中にそのクラスのインスタンスが最大で1つしか存在しないようにします。

final クラスは、クラスの使用法を制御するもうひとつの例です。ファイナルクラスでは、サブクラスを作成することができません。ファイナルクラスは、セキュリティのためにいくつかのフレームワークで使用され、クラスがその元の属性を保持することを保証します。

以下では、メタクラスを使ってクラスを継承させないようにするfinalクラスの実装を紹介します。

# final.py


# a final metaclass. Subclassing a class that has the Final metaclass should fail
class Final(type):
    def __new__(cls, name, bases, attr):
        # Final cannot be subclassed
        # check that a Final class has not been passed as a base
        # if so, raise error, else, create the new class with Final attributes
        type_arr = [type(x) for x in bases]
        for i in type_arr:
            if i is Final:
                raise RuntimeError("You cannot subclass a Final class")
        return super(Final, cls).__new__(cls, name, bases, attr)


# Test: use the metaclass to create a Cop class that is final


class Cop(metaclass=Final):
    def exit():
        print("Exiting...")
        quit()


# Attempt to subclass the Cop class, this should idealy raise an exception!
class FakeCop(Cop):
    def scam():
        print("This is a hold up!")


cop1 = Cop()
fakecop1 = FakeCop()


# More tests, another Final class
class Goat(metaclass=Final):
    location = "Goatland"


# Subclassing a final class should fail
class BillyGoat(Goat):
    location = "Billyland"


このコードでは、Final クラスをサブクラス化するためのクラス宣言を含んでいます。これらの宣言は失敗し、例外がスローされる結果となります。クラスのサブクラス化を制限するメタクラスを使用することで、コードベースに final クラスを実装することができます。

メタクラス 例2:操作の実行時間を追跡するクラスの作成

プロファイラは、コンピューティング・システムにおけるリソースの使用状況を把握するために使用されます。プロファイラーは、メモリー使用量や処理速度などの技術的な指標を追跡することができます。

メタクラスを使用すると、コードの実行時間を追跡することができます。このコード例は完全なプロファイラーではありませんが、プロファイラーのような機能を実現するためのメタプログラミングの方法を示す概念的な例です。

# timermetaclass.py
import types


# A timer utility class
import time


class Timer:
    def __init__(self, func=time.perf_counter):
        self.elapsed = 0.0
        self._func = func
        self._start = None


def start(self):
        if self._start is not None:
            raise RuntimeError('Already started')
        self._start = self._func()


def stop(self):
        if self._start is None:
            raise RuntimeError('Not started')
        end = self._func()
        self.elapsed += end - self._start
        self._start = None


def reset(self):
        self.elapsed = 0.0


@property
    def running(self):
        return self._start is not None


def __enter__(self):
        self.start()
        return self


def __exit__(self, *args):
        self.stop()


# Below, we create the Timed metaclass that times its classes' methods
# along with the setup functions that rewrite the class methods at
# class creation times


# Function that times execution of a passed in function, returns a new function
# encapsulating the behavior of the original function
def timefunc(fn, *args, **kwargs):


def fncomposite(*args, **kwargs):
        timer = Timer()
        timer.start()
        rt = fn(*args, **kwargs)
        timer.stop()
        print("Executing %s took %s seconds." % (fn.__name__, timer.elapsed))
        return rt
    # return the composite function
    return fncomposite


# The 'Timed' metaclass that replaces methods of its classes
# with new methods 'timed' by the behavior of the composite function transformer
class Timed(type):


def __new__(cls, name, bases, attr):
        # replace each function with
        # a new function that is timed
        # run the computation with the provided args and return the computation result
        for name, value in attr.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                attr[name] = timefunc(value)


return super(Timed, cls).__new__(cls, name, bases, attr)


# The below code example test the metaclass
# Classes that use the Timed metaclass should be timed for us automatically
# check the result in the REPL


class Math(metaclass=Timed):


def multiply(a, b):
        product = a * b
        print(product)
        return product


Math.multiply(5, 6)


class Shouter(metaclass=Timed):


def intro(self):
        print("I shout!")


s = Shouter()
s.intro()


def divide(a, b):
    result = a / b
    print(result)
    return result


div = timefunc(divide)
div(9, 3)


ご覧のように、オンザフライでクラスを書き換える Timed メタクラスを作成することができました。Timedメタクラスを使用する新しいクラスが宣言されるたびに、そのメソッドはタイマーユーティリティクラスによって時間を計るように書き直されます。Timed クラスを使用して計算を実行すると、余計なことをしなくても、自動的にタイミングをとってくれます。

メタプログラミングは、Webフレームワークやデバッガなど、他の開発者が使うコードやツールを書いている場合に、とても便利なツールです。コード生成とメタプログラミングを使えば、あなたのコードライブラリを利用するプログラマの生活を楽にすることができます。

お勧めのコース

マスタリングPython

メタクラスの力を使いこなす

メタクラスとメタプログラミングには多くのパワーがあります。欠点は、メタプログラミングがかなり複雑になりうることです。多くの場合、デコレーターを使用することで、よりシンプルな方法でエレガントな解決策を得ることができます。メタクラスは、単純さよりも汎用性が求められる場合に使用すべきです。

メタクラスを効果的に利用するために、 Python 3 のメタクラスに関する公式のドキュメントを読むことをお勧めします。

を参照してください。

タイトルとURLをコピーしました