Pythonのデコレータとメタクラスの高度な使用法
概要
Pythonのメタクラスを紹介しているうちに、Pythonの最も強力な機能の大きな問題は、プログラマがそれらがどのように彼らの通常のタスクを簡素化することができるかを認識していないことであることに気づきました。そのため、メタクラスのような機能は、本当のゲームチェンジャーではなく、標準的なOOP言語に追加された、派手だけどどちらかというと使い勝手の悪いものと考えられています。
この記事では、メタクラスとデコレータを使って、継承したり、デコレーションされたメソッドを簡単に追加してカスタマイズできる強力なクラスを作成する方法を紹介します。
メタクラスとデコレータ: 天が作りし組み合わせ
メタクラスは複雑なテーマであり、高度なプログラマーであっても、メタクラスの幅広い実用的な使い方を理解していないことがほとんどです。Python(あるいはSmalltalkやRubyのようにメタクラスをサポートする他の言語)は、C++やJavaに見られる「標準的な」オブジェクト指向のパターンやソリューションに最も適合しない部分である可能性があります。
実際、メタクラスが活躍するのは、多くの自動化を必要とする高度なライブラリやフレームワークをプログラミングするときです。例えば、 Django のフォームシステムは、その魔法を提供するためにメタクラスに大きく依存しています。
しかし、私たちは通常、馴染みのない技術を「マジック」や「トリック」と呼ぶことに注意しなければなりません。その結果、Pythonでは他の言語に比べて実装が特殊なため、多くのものがこのように呼ばれます。
あなたのプログラミングにスパイスを加える時が来ました:Pythonのマジックを練習して、この言語の力を利用しましょう。
今回の記事では、デコレータとメタクラスの面白い共同利用を紹介します。デコレータを使ってメソッドをマークし、与えられた操作を行う際にクラスが自動的に使用できるようにする方法を紹介します。
具体的には、文字列を「処理」するために呼び出されるクラスを実装し、単純なデコレーションされたメソッドを使ってさまざまな「フィルタ」を実装する方法を紹介します。私が得たいのは次のようなものです。
code: python
class MyStringProcessor(StringProcessor):
@stringfilter
def capitalize(self, str):
@stringfilter
def remove_double_spaces(self, str):
msp = MyStringProcessor()
"A test string" == msp("a test string")
モジュールは StringProcessor クラスを定義しており、これを継承して、標準的なシグネチャ (self, str) を持ち、@stringfilter でデコレートされたメソッドを追加してカスタマイズすることができます。このクラスは後でインスタンス化され、そのインスタンスを使って文字列を直接処理し、結果を返すことができます。内部的には、クラスは自動的にすべての装飾されたメソッドを連続して実行します。また、このクラスは、私がフィルターを定義した順番に従うようにしたいと思います:最初に定義され、最初に実行されます。
メタクラスへのヒッチハイク・ガイド
この目標を達成するために、メタクラスはどのように役立つのでしょうか?
簡単に言えば、メタクラスは、クラスを得るためにインスタンス化されるクラスです。つまり、クラスをインスタンス化するなどして使うときには、まずPythonがメタクラスと私たちが書いたクラス定義を使ってそのクラスを構築するのです。例えば、クラスのメンバーは __dict__ 属性で見つけることができるのはご存知だと思います。この属性は、標準的なメタクラスである type によって作成されます。
そう考えると、クラスの定義の中に関数のサブセットを特定するためのコードを挿入するには、メタクラスが良い出発点になります。言い換えれば、メタクラスの出力(つまりクラス)は、標準的なケースとまったく同じように構築したいのですが、@stringfilter でデコレートされたすべてのメソッドの別個のリストが追加されます。
クラスには名前空間があり、それはクラス内で定義されたものの辞書であることはご存知でしょう。そのため、標準の型メタクラスを使用してクラスを作成すると、クラス本体が解析され、名前空間を収集するためにdict()オブジェクトが使用されます。
しかし、私たちは定義の順序を保持することに興味があり、Pythonの辞書は順序付けられていない構造であるため、Python 3でクラス作成プロセスに導入された__prepare__()フックを利用します。この関数は、メタクラスに存在する場合、クラスの前処理に使用され、名前空間をホストするために使用される構造体を返すために使用されます。そこで、公式ドキュメントにある例に従って、次のようなメタクラスの定義を始めます。
code: python
class FilterClass(type):
def __prepare__(name, bases, **kwds):
return collections.OrderedDict()
こうすることで、クラスが作成されるときに、OrderedDictが名前空間をホストするために使用され、定義の順序を維持することができます。シグネチャ__prepare__(name, bases, **kwds) は、言語によって強制されることに注意してください。メソッドに第一引数としてメタクラスを取得させたい場合(メソッドのコードがメタクラスを必要とするため)、シグネチャを__prepare__(metacls, name, bases, **kwds) に変更し、@classmethod で装飾しなければなりません。
メタクラスで定義したい2つ目の関数は __new__() です。クラスのインスタンス化と同じように、このメソッドはPythonによってメタクラスの新しいインスタンスを取得するために呼び出され、__init__() の前に実行されます。このメソッドのシグネチャは __new__(metacls, name, bases, namespace, **kwds) でなければならず、結果はメタクラスのインスタンスとなります。通常のクラスに対応するものとして、__new__() は通常、親クラスの同じメソッドをラップしますが、この場合はtypeに独自のカスタマイズを加えています。
必要なカスタマイズは、何らかの方法でマークされたメソッドのリストを作成することです(装飾されたフィルタ)。簡単にするために、装飾されたメソッドには _filter という属性があるとします。
完全なメタクラスは次のようになります。
code: python
class FilterClass(type):
@classmethod
def __prepare__(name, bases, **kwds):
return collections.OrderedDict()
def __new__(metacls, name, bases, namespace, **kwds):
result = type.__new__(metacls, name, bases, dict(namespace))
result._filters = [
value for value in namespace.values() if hasattr(value, '_filter')]
return result
今度は、すべてのフィルター・メソッドを _filter 属性でマークする方法を見つけなければなりません。
華麗なデコレータの解体新書
デコレータ(Decorate): 物体や場所に何かを加える、特に魅力的にするために(ケンブリッジ辞書)
デコレーターは、その名が示すように、関数やメソッドを補強するための最良の方法です。デコレーターは基本的に、別の呼び出し可能なものを受け入れて処理し、それを返す呼び出し可能なものであることを覚えておいてください。
デコレータは、メタクラスと組み合わせて使用することで、コードに高度な動作を実装するための非常に強力で表現力豊かな手段となります。今回の例では、デコレータの最も基本的な作業である、デコレートされたメソッドへの属性の追加を簡単に行うことができます。
通常、デコレータはクラスで実装することが多いのですが、 @stringfilter デコレータは関数で実装することにしました。その理由は、 デコレータクラスが引数のないデコレータの実装に使われた場合と、 引数のあるデコレータの実装に使われた場合とで、 挙動が異なるからです。今回のケースでは、この違いのために複雑なコードを書かなければならず、その説明は今はやり過ぎだと思います。デコレーターの詳細については、今後の記事で紹介しますが、それまでは参考文献に挙げたBruce Eckelの3つの記事をチェックしてください。
デコレーターはとてもシンプルです。
code: python
def stringfilter(func):
func._filter = True
return func
ご覧のように、デコレーターは _filter という属性を関数に作成するだけです (関数はオブジェクトであることを忘れないでください)。この属性の実際の値は、このケースでは重要ではありません。 なぜなら、この属性を含むクラス・メンバーを見分けることに興味があるだけだからです。
呼び出し可能オブジェクトの力学
私たちは、関数を「呼び出される(Called)」または「実行される(Executed)」特別な言語コンポーネントとして考えることに慣れています。Pythonでは、関数は他のものと同じようにオブジェクトであり、関数を実行できる機能は __call__() メソッドの存在に由来します。Pythonは設計上ポリモーフィック(polymorphic) であり、委譲(Delegation)に基づいているため、コード内で起こる(ほとんど)すべてのことが、対象となるオブジェクトの何らかの機能に依存しています。
この一般化の結果、__call__() メソッドを含む全てのオブジェクトは、関数のように実行され、呼び出し可能なオブジェクトの名前を得ることができます。
StringProcessorクラスは、このようにこのメソッドを含み、そこに含まれる全てのフィルターで文字列処理を実行します。そのコードは次のようになります。
code: python
class StringProcessor(metaclass=FilterClass):
def __call__(self, string):
_string = string
for _filter in self._filters:
_string = _filter(self, _string)
return _string
このシンプルな関数を簡単に見てみると、文字列を引数として受け取り、それをローカル変数に格納し、フィルタをループさせて、ローカルの文字列、つまり前のフィルタの結果に対してそれぞれのフィルタを実行していることがわかります。
フィルタ関数は self._filters リストから抽出され、これはすでに説明した FilterClass メタクラスによってコンパイルされます。
今やるべきことは、メタクラスの機械と __call__() メソッドを得るために StringProcessor を継承し、@stringfilter デコレーターで装飾しながら、必要なだけのメソッドを定義することです。
デコレータとメタクラスのおかげで、検討中のデコレータで装飾されていない限り、文字列処理に干渉しない他のメソッドをクラス内に持つことができることに注意してください。
派生クラスの例は次のようになります。
code: python
class MyStringProcessor(StringProcessor):
@stringfilter
def capitalize(self, string):
return string.capitalize()
@stringfilter
def remove_double_spaces(self, string):
return string.replace(' ', ' ')
capitalize()とremove_double_spaces()の2つのメソッドは装飾されているので、クラスを呼び出すときに渡された文字列に順番に適用されます。この最後のクラスの簡単な例がこれです。
code: python
>> import strproc
>> msp = strproc.MyStringProcessor()
>> input_string = "a test string"
>> output_string = msp(input_string)
>> print("INPUT STRING:", input_string)
INPUT STRING: a test string
>> print("OUTPUT STRING:", output_string)
OUTPUT STRING: A test string
>>
最後に
この記事では、メタクラスが何の役に立つか、そしてなぜPythonプログラマの武器の一部になるべきだと思うかについて、実践的な例を示すことを目的としています。
更新 RedditやLinkedinの開発者の中には、メタクラスを使わなくても完璧に実装できる例や、メタクラスの危険な性質について、この投稿の内容に異議を唱える人もいました。私はすべての人から学ぼうとしているので、彼らの提案に感謝しています。
特に、開発者の中には、メタクラスの使用は、クラスの構造や基礎となる機構の多くを隠してしまうため、リスクの伴うと考える人がいることは興味深いことです。その通りなので、(他の技術でもそうですが)メタクラスを使う理由をよく考えて、それをよく理解した上で使うようにしてください。
トリビア
原文のセクションのタイトルは以下の本から来ています。
A Match Made in Space - George McFly
Metaclasses and decorators: a match made in space / メタクラスとデコレータ: 天が作りし組み合わせ
映画「Back to the Futures] の1985年に戻ったラストシーンで、歴史が変わりセレブ小説家となった父ジョージの元に届けられた処女作が「A MATCH MADE IN SPACE」となっています。
The Hitchhiker's Guide To the Galaxy - Various Authors
The Hitchhiker's Guide To Metaclasses / メタクラスへのヒッチハイク・ガイド
The Anatomy of Purple Dragons - Unknown
The Anatomy of Purple Decorators / 華麗なデコレータの解体新書
Purple Dragone は 『ダンジョンズ&ドラゴンズ』の世界に登場するドラゴンの一種。
The Dynamics of an Asteroid - James Moriarty」
The Dynamics of a Callable Object / 呼び出し可能オブジェクトの力学
訳注:意訳しようとも考えましたが、著者のウィットに飛んだ遊び心を残したかったので、できるだけ原文よりに訳してみました。
ソースコード
オンライン・リソース
以下のリソースが役に立つかもしれません。
メタクラス
デコレータ
呼び出し可能オブジェクト
訳注: 原文のJeff Knupp 氏と Rafe Kattler 氏の資料はリンク切れとなっていました。内容が近く該当するであろう資料へのリンクに差し替えています。