2008年02月27日

メタクラスで遊ぶ(2)

 実は前回は、メタクラス(カスタムメタクラス)で一番力を発揮する__new__メソッドの話と、__call__メソッドの話をわざと省いた。
 いや、えらそうなコトを言っておきながら、自分でも少し混乱していたので、こっそりと色々試していたのだ。
 で、確認終了したので、ここにまとめる。
 (2007.3.2修正しました)
■__new__メソッドについて
 メタクラスの話に入る前に、確認を兼ねて__new__組み込みメソッドについて。
 Pythonでクラスを定義する場合は、__init__メソッドで初期化するのが普通だが、生成過程を制御したい場合には、__new__組み込みメソッドを使用する。
 __new__は、__init__と全く同様に引数を設定し、クラスオブジェクトの関数呼び出しの形で使用される。
 ただし、二点ほど注意することがある。  一点は「戻り値を設定しない」__init__とは異なり、__new__は戻り値が設定できる。
 ここで、やろうと思えば、自分自身(インスタンスオブジェクト)以外のものを返すことも出来るのだ。
 むしろ、素直にインスタンスオブジェクトを返すなら、__init__で十分なので、__new__を設定する場合は、色々「違うもの」を返す設定を行うことが多い。
 __init__は、__new__の戻り値がなんであろうと、__new__の戻り値に対して適用される。
 二点目は、__new__はクラスメソッドであるということである。
 よって、__new__の第一引数は、インスタンスではなくクラスである。
 以下に、__new__のデフォルト状態を仮に記述してみる。
class Class: # 2.xならClass(object)
  def __new__(cls, *args):
    ins = object.__new__(cls, *args)
    return ins
  def __init__(self, *args):
    ...
 戻り値を設定しないと、インスタンスの代わりにNoneが戻ってしまうので、要注意。



■オブジェクト生成の手順
 typeを継承したカスタムメタクラスをMetaClass、このメタクラスから生成されるクラスをClassとすると、実行順序は以下の通りになる。
  1. MetaClass.__new__(Class定義時)
  2. MetaClass.__call__(インスタンス生成時)
  3. Class.__new__(インスタンス生成時)
  4. Class.__init__(インスタンス生成時)
1.MetaClass.__new__:初期化の際に実行される。つまり「クラスが定義された」時点で実行される。typeの__new__メソッドは基本的に自身(一般で言うところのselfだが、メタクラスではclsと書くらしい)のほかに3変数(クラス名、親クラスタプル、属性辞書)を取る。よって、ここでよほど特殊な処理(クラスオブジェクトを作らないなど)をしない限り、以下のように書くのが普通。
class MetaClass(type):
  def __new__(cls, name, bases, dict):
    <処理内容>
    return type.__new__(cls, name, bases, dict)
2.MetaClass.__call__:カスタムメタクラスのインスタンスであるクラスが、インスタンスを作る際に実行される。クラスオブジェクトがインスタンスメソッドを作成する際は、ご存知の通りクラスオブジェクトを関数のように呼び出すので、この動作を定義しているだけである。このときのクラスオブジェクトはメタクラスのインスタンスなので、クラスでインスタンスの関数的呼び出しを定義してるときと全く同じだと思えばよい。ここでも、通常の処理を行うなら、以下のように行う。
class MetaClass(type):
  def __call__(cls, *args):
    ins = object.__new__(cls)
    cls.__init__(ins, *args)
    return ins
 MetaClass.__call__は、自動的にクラスの__init__は呼ばないので、__init__を利用するなら、それを明示的に返す必要がある。
 最初の*argsは、任意引数リストだが、type.__call__に渡す*argは、引数リストのアンパックである(同じ書き方だが、用法は全く逆)
 Pythonistaにはおなじみの書き方だが、それ以外の方は少し驚くかもしれない。
 メタクラスの実装の仕方は、前回も紹介したが、以下の通り。
class CLASS(metaclass = MetaClass):
 動作を確認するために、以下のような実験をしてみた。
>>> class MetaClass(type):
...   def __new__(cls, name, bases, dict):
...     print('metaclass new')
...     return type.__new__(cls, name, bases, dict)
...   def __call__(cls, *args):
...     print('metaclass call')
...     return type.__call__(cls, *args)
...
>>> class CLASS(metaclass = MetaClass):
...   def __new__(self, *args):
...     print('CLASS new')
...     return self
...
metaclass new
>>> c = CLASS()
metaclass call
CLASS new
>>> c
<class '__main__.CLASS'>
>>>
 メタクラスで定義されたクラスオブジェクトの__new__は、クラスオブジェクトが定義された直後に実行されるているのが、メッセージで確認できる。
 クラスオブジェクトで定義されたインスタンスオブジェクトの__new__は、メタクラスで定義されたクラスオブジェクトの__call__の後に実行される。
 この順番さえちゃんと理解していれば、いろいろと楽しいイタズラができることになる。



■デザインパターンのSingletonを作ってみる
 前回の追加コメントで紹介したページには、クックブックに載っていたというSingletonの実装例が載っていた(一部コードにバグがあったが、動いてしまうので判り難い……どこかは、上のコードを見て見つけて欲しい☆)
 今回私も、少しそのコードを改良してSingletonを作ってみよう。
※Singleton……デザインパターンの一種で、あるクラスのインスタンスの実体が1つであることを保証するクラス。何回インスタンスを作っても、最初の一個のコピーを返す。実際の利用法以上に、名前がなんだか格好いいので、みんなよく例として作っている。
# metatest.py
class Singleton(type):
    def __new__(cls, name, bases, dict):
        dict['instance'] = None
        return type.__new__(cls, name, bases, dict)
    
    def __call__(cls, *args):
        if cls.instance == None:
            cls.instance = type.__call__(cls, *args)
        return cls.instance

class X(metaclass = Singleton):
    def __init__(self, *args):
        self.lst = args

class Y(metaclass = Singleton):
    def __new__(self, *args):
        self.lst = args
        return self

class Xx(X):
    pass


>>> from metatest import *
>>> x = X(1,2,3)
>>> x2 = X(4,5,6)
>>> x.lst
(1, 2, 3)
>>> x2.lst
(1, 2, 3)
>>> x2.a = 666
>>> x.a
666
>>> y = Y(5,5,5)
>>> y.lst
(5, 5, 5)
>>> xx = Xx()
>>> xx.lst
()
>>> x.lst
(1, 2, 3)
>>>
 同じクラスから作ったインスタンスは同じオブジェクトになっていることが、ご理解いただけただろうか。
(このコードにはバグがありますが、あえて残します。後日別記事で修正します)
 同じメタクラスであっても、違うクラスは違うインスタンスを、子クラスもやはり別インスタンスを生成する。



■おまけ  最後にはやっぱり、バカネタを一つ。
>>> class M(type):
...   def __call__(cls, *args):
...     print('「クラス生成は失敗しました」')
...     return
...
>>> class X(metaclass = M):
...   pass
...
>>> x = X()
「クラス生成は失敗しました」
>>> x
>>>
 あるメタクラスからクラスを作ると、必ず失敗するメタクラス。
 何の使い道も無さそうだが、実はトップダウン開発のスタブなんかで使えそうだ。
 メタクラスをかませることで、それぞれ下位のクラスの振る舞いを完全にコントロールできる。
 スーパークラスでは、オーバーライドしてしまう危険性があるが、メタクラスでクラス生成段階をコントロールすれば、この危険性は無い。
 そして、下位クラスの準備が整ったら、メタクラスを書き換えてコントロールを外せばいい。
 異常が出れば、すぐにスタブに切り替えられるオブジェクトってのは、なかなか良いのでは?

この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/kikwai/51280093
この記事へのトラックバック
[Python][Mobile]WSGIUserAgentMobile rev.51 [Python][PIL][PyQt4]ImageQt [FreeBSD] lang/python25 [プログラミング][Python][wxPython] Working with Windows メタクラスで遊ぶ(2) 初期化を手抜き [Python]続・再帰 Meadow+Python-modeでPythonを書いている。 「Pyth
[Python]巡回【常山日記】at 2008年02月27日 15:34
この記事へのコメント
今pythonの勉強中です。
ここのページを昨日初めて知りまして色々勉強になります。
__new__の意味がよく分からなかったのですが、
__new__:クラスから新たにインスタンスを生成する
__ini__:インスタンスの初期化をする
ということが理解できてきました。
試行錯誤して以下のプログラムを作成したのですが、
ここで object.__new__(cls) = self のようです。

class Class(object):
def __new__(cls):
print "new"
print type(cls)
return object.__new__(cls)
def __init__(self):
print "init"
print type(self)
object.__init__(self)

Class()
Posted by envproblem at 2008年03月01日 07:24
 あ、あれ?

 見直してみたら、__new__の説明のトコで、一部変なコト書いてますね。

 直しておきます、はい。

※リダイレクトしてるのに勘違いしてるし……
Posted by 機械伯爵 at 2008年03月01日 23:33
>> envproblemさん

 修正しました。
 ご指摘、どうもありがとうございました。
Posted by 機械伯爵 at 2008年03月02日 09:33