FlyweightパターンをPythonic(?)にする(3)


[前前回](http://qiita.com/FGtatsuro/items/9e45ee7a0e4165004dc0)、[前回](http://qiita.com/FGtatsuro/items/3313fe79785f94e7d260)に引き続き、[こちら](http://yloiseau.net/articles/DesignPatterns/flyweight/)の記事を解釈した際のメモ。

## メタクラスを用いたFlyweightパターン(typeクラスのサブクラス)

メタクラスを用いた方法の前に前提となる`type`クラスについて触れておく。`type`クラスはクラス定義(=クラスオブジェクト)を生成する際に用いられる。

```python:
def methodA(self):
    print('methodA')

# クラス名、親クラス(複数指定可)、名前空間(辞書により表現する、ここでは'methodA'にmethodA関数を紐付けている)を引数に取り
# クラスオブジェクトを生成する
ClassA = type('ClassA', (object,), {'methodA': methodA})
a = ClassA()
a.methodA()

# 以下の定義と同等
# class ClassA(object):
#     def methodA(self):
#        print('methodA')

```

前述の通り、`type`はクラスである。そのためサブクラスを作成することができる。
サブクラスは`type`クラスの代わりにクラスオブジェクト生成に使用でき、`__new__`メソッドや`__init__`メソッドをオーバーライドすることで、クラスオブジェクト生成時に処理を追加できる。

```python:
def methodA(self):
    print('methodA')

class Meta(type):
    def __new__(cls, name, bases, namespace):
        # クラスオブジェクト生成時に追加の処理を差し込める
        print('hoge')
        return super().__new__(cls, name, bases, namespace)

# typeクラスの代わりにMetaクラス(=typeクラスのサブクラス)を使用できる
ClassA = Meta('ClassA', (object,), {'methodA': methodA})
a = ClassA()
a.methodA()
```

通常のクラス定義構文でクラスオブジェクト生成時にtypeクラス以外を使用したい場合、Python3では`metaclass`キーワードを使用する。この時に指定するクラスをメタクラスと呼ぶ。

```python:
class Meta(type):
    def __new__(cls, name, bases, namespace):
        # クラスオブジェクト生成時に追加の処理を差し込める
        print('hoge')
        return super().__new__(cls, name, bases, namespace)

class ClassA(object, metaclass=Meta):
# Python2のメタクラスの指定
#class ClassA(object):
#    __metaclass__ =Meta
    def methodA(self):
        print('methodA')

a = ClassA()
a.methodA()
```

以下は、「クラスオブジェクト生成時に処理を追加できる」というメタクラスの性質を利用して、Flyweightパターンのロジックをクラスオジェクトの生成時に差し込む例である。

```python:
class MetaFlyweight(type):
    def __new__(cls, name, bases, namespace):
        # typeクラスの__new__メソッドはクラスオブジェクトを返す
        clsobj = type.__new__(cls, name, bases, namespace)

        clsobj._instances = {}
        def _get_instance(cls, *args, **kargs):
            instance = super(cls, cls).__new__(cls)
            return cls._instances.setdefault(
                    (args, tuple(kargs.items())), instance)
        # staticメソッドにしなくても動くが、以下の記述に従いstaticメソッドにしておく
        # (ref.) https://docs.python.org/3.3/reference/datamodel.html#object.__new__
        clsobj.__new__ = staticmethod(_get_instance)
        return clsobj

class Hoge(object, metaclass=MetaFlyweight):
# Python2のメタクラスの指定
#class Hoge(object):
#    __metaclass__ = MetaFlyweight
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(object, metaclass=MetaFlyweight):
# Python2のメタクラスの指定
#class Piyo(object):
#    __metaclass__ = MetaFlyweight
    def __init__(self, args1, kwargs1):
        self.args1 = args1
        self.kwargs1 = kwargs1

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b') is Piyo('a', kwargs1='b')
assert Piyo('a', kwargs1='b') is not Piyo('a', kwargs1='c')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')
```

## メタクラスを用いたFlyweightパターン(関数)

しかし必ず`type`クラスのサブクラスを生成する必要はない。
クラスオブジェクト生成時には、「クラス名(文字列)」「親クラス(タプル)」「名前空間(辞書)」を与えたtypeクラスが呼び出される。ただしここで必要なのは`type`クラス(もしくはサブクラス)というよりも、「上記3つの引数を受け取るcallableなオブジェクト」なのだ。そのため、以下のように関数をメタクラスに指定することも可能である。

```python:
def meta_flyweight(name, bases, namespace):
    clsobj = type(name, bases, namespace)

    clsobj._instances = {}
    def _get_instance(cls, *args, **kargs):
        instance = super(cls, cls).__new__(cls)
        return cls._instances.setdefault(
                (args, tuple(kargs.items())), instance)
    # staticメソッドにしなくても動くが、以下の記述に従いstaticメソッドにしておく
    # (ref.) https://docs.python.org/3.3/reference/datamodel.html#object.__new__
    clsobj.__new__ = staticmethod(_get_instance)
    return clsobj

class Hoge(object, metaclass=meta_flyweight):
# Python2のメタクラスの指定
#class Hoge(object):
#    __metaclass__ = meta_flyweight
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(object, metaclass=meta_flyweight):
# Python2のメタクラスの指定
#class Piyo(object):
#    __metaclass__ = meta_flyweight
    def __init__(self, args1, kwargs1):
        self.args1 = args1
        self.kwargs1 = kwargs1

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b') is Piyo('a', kwargs1='b')
assert Piyo('a', kwargs1='b') is not Piyo('a', kwargs1='c')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')
```

*元の記事では更にlamdaを用いた例を紹介しているが、これはさすがにトリッキーなので省く。