2010年10月31日日曜日

Python 3のデザインパターンでメタクラスしてみた(1)

必要もないのにPython 3とデザインパターンとメタクラスを使うのはPythonプログラマ的厨二病の定番らしいので、とりあえず全部混ぜてエントリを書いてみました。(挨拶)

Pythonでデザインパターンを試す場合はGoFSingleton パターンが定番らしいのですが、今回は私の好みでFlyweight パターンを選択しました。
Flyweight パターンの詳細については、恐れ入りますがWikipediaの該当項目を参照してください。



1. Flyweightクラス
Python向けのFlyweight パターンのサンプルコードはいくつかあるようなのですが、今回は、Duncan Booth氏の「Patterns in Python」で紹介されているサンプルコードをお借りしました。Python 3向けに修正したのが下記の flyweight.py です。
class Flyweight:
    _InstancePool = {}

    def __new__(cls, name):
        print("Flyweight.__new__({}, {})".format(cls, repr(name)))
        obj = Flyweight._InstancePool.get(name, None)

        if not obj:
            obj = object.__new__(cls)
            Flyweight._InstancePool[name] = obj

        print("  Flyweight.return {}".format(obj))
        return obj

    def __init__(self, name):
        print("Flyweight.__init__({}, {})".format(self, repr(name)))
        self.name = name

 

Flyweightクラスは引数を1つ受け取って、その引数でインスタンスを生成した事があれば同じインスタンスを、生成した事がなければ新たに生成したインスタンスを返す仕様になっています。
このクラスの本体は、1つのクラス変数と2つの特殊メソッドですので順番に説明します。

・_InstancePool
_InstancePoolはFlyweightクラスのクラス変数でdict型です。
この変数はFlyweightパターンで一度生成した事があるインスタンスを記録するのに使います。

元サンプルでは、生成したインスタンスを必要に応じてガベージコレクタの回収対象にするために、weakrefモジュールのweakref.WeakRefDictionary()が使われているのですが、今回は単なるdict型にしています。

ですから、一度生成されたインスタンスはガベージコレクタの対象にはならず、ずっと残り続けることになります。
初期化コストが大きいオブジェクトを使い回したい場合はその方が使い勝手が良くなる場合もあると思うのですが、これではFlyweightパターンとは呼べないかもしれません。

・__init__() メソッド
今回は生成されたインスタンスのオブジェクトIDとメソッドの引数を表示して、与えられた引数をself.nameに保存しているだけです。

・__new__() メソッド
今回の肝です。Flyweightクラスは引数を1つ受け取って、一度生成した事があるオブジェクトであればそのオブジェクトを、生成した事がないオブジェクトであれば新たに生成したオブジェクトを返します。

では試しにFlyweightクラスを使ってみましょう。
>>> from flyweight import Flyweight
>>> a = Flyweight('a')
Flyweight.__new__(<class 'flyweight.Flyweight'>, 'a')
  Flyweight.return <flyweight.Flyweight object at 0x100510e90>
Flyweight.__init__(<flyweight.Flyweight object at 0x100510e90>, 'a')
>>> b = Flyweight('b')
Flyweight.__new__(<class 'flyweight.Flyweight'>, 'b')
  Flyweight.return <flyweight.Flyweight object at 0x100550a10>
Flyweight.__init__(<flyweight.Flyweight object at 0x100550a10>, 'b')
>>> a_clone = Flyweight('a')
Flyweight.__new__(<class 'flyweight.Flyweight'>, 'a')
  Flyweight.return <flyweight.Flyweight object at 0x100510e90>
Flyweight.__init__(<flyweight.Flyweight object at 0x100510e90>, 'a')
最後の a_cloneのオブジェクトIDが、最初のaと同じオブジェクトIDになっていることに注目してください。この振る舞いがFlyweight パターンによるものです。

尚、__new__() メソッドと __init__() メソッドは両方ともインスタンスの生成時に呼び出される特殊メソッドですが、__new__() メソッドはインスタンスの生成処理そのものに使われ、__init__() メソッドは生成されたインスタンスの初期化処理で使われるという違いがあります。

今回の処理に、__new__() メソッドを使っているのは、Flyweight パターンの同じインスタンスを使い回すかどうかのチェックはインスタンスを生成する前に行わなければいけないので、本来生成しなくても良かった新しいインスタンスが生成された後に __init__() メソッドでチェックしても遅いからです。

__init__() メソッドはNoneオブジェクト以外を返すとエラーになりますが、__new__() メソッドは何を返しても構わないので、引数チェックで問題があった場合にNoneオブジェクトを返したり、他のクラスのインスタンスを返すような事もできてしまいます。

2. Flyweightクラスの継承
次にこのFlyweightクラスを継承する2つのクラス、ClassAクラスとClassBクラスを用意します。
下記のコードをflyweight.pyに追記しました。

class ClassA(Flyweight):
    def __init__(self, name):
        print("ClassA.__init__({}, {})".format(self, repr(name)))
        self.name = name


class ClassB(Flyweight):
    def __init__(self, name):
        print("ClassB.__init__({}, {})".format(self, repr(name)))
        self.name = name

さっそくテストをしてみます。

>>> from flyweight import ClassA, ClassB
>>> A_a = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550a10>
ClassA.__init__(<flyweight.ClassA object at 0x100550a10>, 'a')
>>> A_b = ClassA('b')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'b')
  Flyweight.return <flyweight.ClassA object at 0x100550990>
ClassA.__init__(<flyweight.ClassA object at 0x100550990>, 'b')
>>> A_a_clone = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550a10>
ClassA.__init__(<flyweight.ClassA object at 0x100550a10>, 'a')

ここまでは上手く行きました。
前回は Flyweight.__init__ だった部分が、今回は ClassA.__init__ になっていますが、これは ClassAクラスが Flyweightクラスから継承した Flyweight.__init__()メソッドを自前の__init__()メソッドで上書きしたからですので、問題はありません。
しかし続けてClassBクラスをテストすると、

>>> B_a = ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550a10>
>>> B_c = ClassB('c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
ClassB.__init__(<flyweight.ClassB object at 0x1005509d0>, 'c')
>>> A_c = ClassA('c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
>>> ClassA('a') is ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550a10>
ClassA.__init__(<flyweight.ClassA object at 0x100550a10>, 'a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550a10>
True
>>> ClassB('c') is ClassA('c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
ClassB.__init__(<flyweight.ClassB object at 0x1005509d0>, 'c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
True

のような結果になって、ClassA('a') と ClassB('a') 、ClassB('c') と ClassA('c') が同一のオブジェクトになってしまいました。(ここで引っかかりを感じた方は、_InstancePoolをdict型にした事を思い出してください。)
あえて同一のオブジェクトにしたい場合もあるかもしれませんが、今回はそうではありません。

こうなってしまった原因は、_InstancePoolがFlyweightクラスのクラス変数なので、ClassAクラスの_InstancePool属性とClassBクラスの_InstancePool属性が同一のオブジェクトになってしまい、生成したインスタンスの記録が共有されてしまったからです。
そこで、Flyweightクラスのサブクラスが、それぞれ自前で _InstancePool を持つように修正することにします。
Flyweightクラスを下記のように修正してみました。

class Flyweight:
    def __new__(cls, name):
        print("Flyweight.__new__({}, {})".format(cls, repr(name)))
        if not cls.__dict__.get('_InstancePool'):
            cls._InstancePool = {}

        obj = cls._InstancePool.get(name, None)

        if not obj:
            obj = object.__new__(cls)
            cls._InstancePool[name] = obj

        print("  Flyweight.return {}".format(obj))
        return obj

    def __init__(self, name):
        print("Flyweight.__init__({}, {})".format(self, repr(name)))
        self.name = name

変更点は、クラス変数を Flyweight._InstancePool にする代わりに、Flyweight.__new__() メソッドを呼び出したクラスのクラスオブジェクトである cls を使って、cls._InstancePool にした事、そして cls._InstancePool を各クラス毎に持たせるために、Flyweight.__new__() メソッドの中で初期化した事です。

Flyweight.__new__() メソッドはインスタンスを生成する度に毎回呼び出されるメソッドですので、クラスの__dict__属性を調べて _InstancePool が既に存在するかどうかをチェックするようにしてあります。

では、もう一度テストしてみましょう。

>>> from flyweight import ClassA, ClassB, Flyweight
>>> A_a = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550950>
ClassA.__init__(<flyweight.ClassA object at 0x100550950>, 'a')
>>> A_b = ClassA('b')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'b')
  Flyweight.return <flyweight.ClassA object at 0x100550990>
ClassA.__init__(<flyweight.ClassA object at 0x100550990>, 'b')
>>> A_a_clone = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550950>
ClassA.__init__(<flyweight.ClassA object at 0x100550950>, 'a')
>>> B_a = ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
ClassB.__init__(<flyweight.ClassB object at 0x1005509d0>, 'a')
>>> B_c = ClassB('c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x100550a50>
ClassB.__init__(<flyweight.ClassB object at 0x100550a50>, 'c')
>>> A_c = ClassA('c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassA object at 0x100550a90>
ClassA.__init__(<flyweight.ClassA object at 0x100550a90>, 'c')
>>> Flyweight_c = Flyweight('c')
Flyweight.__new__(<class 'flyweight.Flyweight'>, 'c')
  Flyweight.return <flyweight.Flyweight object at 0x100550b10>
Flyweight.__init__(<flyweight.Flyweight object at 0x100550b10>, 'c')
>>> ClassA('a') is ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550950>
ClassA.__init__(<flyweight.ClassA object at 0x100550950>, 'a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassB object at 0x1005509d0>
ClassB.__init__(<flyweight.ClassB object at 0x1005509d0>, 'a')
False
>>> ClassA('c') is ClassB('c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassA object at 0x100550a90>
ClassA.__init__(<flyweight.ClassA object at 0x100550a90>, 'c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x100550a50>
ClassB.__init__(<flyweight.ClassB object at 0x100550a50>, 'c')
False

今度は大丈夫ですね。ClassA('a') と ClassB('a') 、ClassB('c') と ClassA('c')が異なるオブジェクトIDになりました。

尚、前回のテストでは、ClassB('a') と ClassA('c') で、__init__() メソッドが実行されていませんでしたが、その理由は、それぞれのクラスの__new__() メソッドが自分のクラス以外のインスタンスを返したからです。この場合は __init__() メソッドは呼び出されません。

3. メタクラスへの招待
継承可能なFlyweight パターンがこれで一応完成しました。しかし、不満が一つだけあります。
それは、

if not cls.__dict__.get('_InstancePool'):
            cls._InstancePool = {}

の部分です。インスタンスを生成する度にクラス変数を初期化済みかどうかを毎回判定するのは、ちょっと冗長ではないでしょうか? これを何とかする方法はないのでしょうか?

そんな時に使える手法の一つが、生成されるインスタンスがクラスオブジェクトになるクラス、メタクラスです。

続く

0 件のコメント:

コメントを投稿