2010年11月7日日曜日

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

前回前々回からの続きです。



話をおさらいすると、継承が可能な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


のように書いたのですが、4〜5行目の、

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

の部分で、クラス変数の有無をインスタンスを生成する度に毎回チェックしているので、これをメタクラスで何とかしてみようという話です。

メタクラスはインスタンスがクラスオブジェクトになるクラスですので、普通のクラスで可能な、

  • __new__() メソッドによる、インスタンスそのものを生成する処理のカスタマイズ
  • __init__() メソッドによる、生成したインスタンスの初期設定
  • __call__() メソッドによる、関数のように呼び出されたインスタンスの引数の動的な差し替え

は、メタクラスの場合はインスタンスであるクラスオブジェクトに対してそのまま適用できます。
今回はクラスオブジェクトの生成後に _InstancePool 属性を追加するだけですので、 __init__() メソッドを使いました。

class FlyweightMeta(type):
    def __init__(cls, *args):
        print("FlyweightMeta.__init__({}, {})".format(cls, *args))
        super().__init__(*args)
        cls._InstancePool = {}


class Flyweight(metaclass=FlyweightMeta):
    def __new__(cls, name):
        print("Flyweight.__new__({}, {})".format(cls, repr(name)))
        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


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, Flyweight
FlyweightMeta.__init__(<class 'flyweight.Flyweight'>, Flyweight)
FlyweightMeta.__init__(<class 'flyweight.ClassA'>, ClassA)
FlyweightMeta.__init__(<class 'flyweight.ClassB'>, ClassB)
>>> A_a = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550ad0>
ClassA.__init__(<flyweight.ClassA object at 0x100550ad0>, 'a')
>>> A_b = ClassA('b')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'b')
  Flyweight.return <flyweight.ClassA object at 0x100550b10>
ClassA.__init__(<flyweight.ClassA object at 0x100550b10>, 'b')
>>> A_a_clone = ClassA('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550ad0>
ClassA.__init__(<flyweight.ClassA object at 0x100550ad0>, 'a')
>>> B_a = ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassB object at 0x100550b50>
ClassB.__init__(<flyweight.ClassB object at 0x100550b50>, 'a')
>>> B_c = ClassB('c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x100550b90>
ClassB.__init__(<flyweight.ClassB object at 0x100550b90>, 'c')
>>> A_c = ClassA('c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassA object at 0x100550bd0>
ClassA.__init__(<flyweight.ClassA object at 0x100550bd0>, 'c')
>>> Flyweight_c = Flyweight('c')
Flyweight.__new__(<class 'flyweight.Flyweight'>, 'c')
  Flyweight.return <flyweight.Flyweight object at 0x100550c50>
Flyweight.__init__(<flyweight.Flyweight object at 0x100550c50>, 'c')
>>> ClassA('a') is ClassB('a')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'a')
  Flyweight.return <flyweight.ClassA object at 0x100550ad0>
ClassA.__init__(<flyweight.ClassA object at 0x100550ad0>, 'a')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'a')
  Flyweight.return <flyweight.ClassB object at 0x100550b50>
ClassB.__init__(<flyweight.ClassB object at 0x100550b50>, 'a')
False
>>> ClassA('c') is ClassB('c')
Flyweight.__new__(<class 'flyweight.ClassA'>, 'c')
  Flyweight.return <flyweight.ClassA object at 0x100550bd0>
ClassA.__init__(<flyweight.ClassA object at 0x100550bd0>, 'c')
Flyweight.__new__(<class 'flyweight.ClassB'>, 'c')
  Flyweight.return <flyweight.ClassB object at 0x100550b90>
ClassB.__init__(<flyweight.ClassB object at 0x100550b90>, 'c')
False

問題ないようなので、整理してみました。
#!/usr/bin/env python3.1

class FlyweightMeta(type):
    """This metaclass is used to initialize subclasses of Flyweight class."""
    def __init__(cls, *args):
        super().__init__(*args)
        cls._InstancePool = {}


class Flyweight(metaclass=FlyweightMeta):
    """Implements the Flyweight design pattern.

        >>> class ClassA(Flyweight):
        ...     pass
        ... 
        >>> A_a = ClassA('a')
        >>> A_b = ClassA('b')
        >>> A_a is A_b
        False
        >>> A_a_cloned = ClassA('a')
        >>> A_a is A_a_cloned
        True
        >>> class ClassB(Flyweight):
        ...     pass
        ... 
        >>> B_a = ClassB('a')
        >>> A_a is B_a
        False

    Original code by Duncan Booth <http://www.suttoncourtenay.org.uk/duncan/accu/pythonpatterns.html#id17>
    """
    def __new__(cls, name):
        obj = cls._InstancePool.get(name, None)

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

        return obj


if __name__ == "__main__":
    import doctest
    doctest.testmod()

とりあえず動きましたので、これでよしとします。

本当は、せっかくメタクラスを使ったので、Flyweight.__new__ の中身も FlyweightMeta.__call__ に追い出したかったのですが、上手くいかないパターンが見つかったので没にしました……orz

0 件のコメント:

コメントを投稿