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


[前回](http://qiita.com/FGtatsuro/items/9e45ee7a0e4165004dc0)と同じく、[こちら](http://yloiseau.net/articles/DesignPatterns/flyweight/)の記事を解釈した際のメモ。

## 継承を用いたMixin(クラスメソッド)

原文ではMixinとしているが正直イマイチピンとこない。
「ある機能(メソッド)を提供し、それ単体では使用しない(=インスタンス化しない)継承専用クラス」と捉えれば良いのだろうか。

コード自体は[可変長引数を使ったfactoryの一般化](http://qiita.com/FGtatsuro/items/9e45ee7a0e4165004dc0#可変長引数を使ったfactoryの一般化)と殆ど変わらない。違いは以下の点。

- FlyweightFactoryが提供していた機能が継承により`Hoge`/`Piyo`クラスから使われている
- ディクショナリのキーとして可変長引数だけでなくクラスも使う

```python:
class FlyweightMixin(object):

    _instances = {}

    @classmethod    
    def get_instance(cls, *args, **kwargs):
		# _instanceはクラス変数であり、このクラスを継承した全てのクラスで使い回される。
        # 引数(*args, **kwargs)が同じだがクラスが違うインスタンスを区別するため、ディクショナリのキーとして継承したクラス(cls)も使う必要がある。
        return cls._instances.setdefault((cls, args, tuple(kwargs.items())), cls(*args, **kwargs))

class Hoge(FlyweightMixin):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(FlyweightMixin):
    def __init__(self, args1, kwargs1):
        self.args1 = args1
        self.kwargs1 = kwargs1


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

# 引数は同じだがクラスが違うケース
# ディクショナリのキーとして継承したクラス(cls)も使っていない場合、このアサーションは失敗する。
assert Hoge.get_instance('a', kwargs1='b') is not Piyo.get_instance('a', kwargs1='b')
```

## 継承を用いたMixin(`__new__`メソッド)

さきほどの例は「`FlyweightMixin`クラスは継承専用である」としていたが、生成しようと思えばFlyweightMixinクラスのインスタンスは`FlyweightMixin()`とすれば生成できてしまう。
以下の例ではインスタンス生成時に必ず呼ばれる`__new__`メソッドに処理をうつし、`__new__`メソッドより後に呼ばれる`__init__`メソッドでは例外を投げるようにしている。
`FlyweightMixin()`と呼ぶと例外を投げるため、`FlyweightMixin`クラスを使用するためには、必ず継承してサブクラスで`__init__`メソッドをオーバーライドしてやらなくてはならない。


```python:
class FlyweightMixin(object):

    _instances = {}

    def __init__(self, *args, **kwargs):
        raise NotImplementedError

    def __new__(cls, *args, **kwargs):
        # Python2, Python3ともに動く
        instance = super(type(cls), cls).__new__(cls)
        # Python3のみ動く: Python2は引数を省略したsuper関数の呼び出しをサポートしていない
        # instance = super().__new__(cls)
        # Python2のみ動く: Python3はobject.__new__が可変長引数を受けとれない(理由分からず)
        # instance = super(type(cls), cls).__new__(cls, *args, **kwargs)
        return cls._instances.setdefault((cls, args, tuple(kwargs.items())), instance)

class Hoge(FlyweightMixin):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(FlyweightMixin):
    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')
```

### (補足)`__new__`メソッドの戻り値

通常、`__new__`メソッドは第一引数で与えられたクラスの新しいインスタンスを返す。一方、`FlyweightMixin`の`__new__`メソッドは、一回作成したインスタンスを`_instances`ディクショナリに保存して、二回目以降は保存されたインスタンスを返す。

## デコレータを用いたMixin

継承を用いた方法では親クラス(`FlyweightMixin`)に持たせていたFlyweightパターンのロジックを、デコレータを使って子クラス側に持たせてしまうのが以下の例である。`flyweight`関数は引数に取ったクラスオブジェクトに対して、`_instances`変数(辞書)および`__new__`メソッド(Flyweightパターンのロジックを定義した関数オブジェクト)を代入して返す。

```python:
# classmethod関数によりクラスメソッド化しないとPython2ではエラーとなる。
# (ref.) 
# http://chikatoike.hatenadiary.jp/entry/2013/07/31/125624
# http://momijiame.tumblr.com/post/67251294770/pythone
# https://docs.python.org/3/whatsnew/3.0.html#operators-and-special-methods
# (2015/02/27 追記)
# staticmethod関数でも大丈夫だった。ドキュメント見ると__new__にいれるのはstaticメソッドの方が妥当?
# (ref.) https://docs.python.org/3.3/reference/datamodel.html#object.__new__
@classmethod
def _get_instance(cls, *args, **kwargs):
    instance = super(type(cls), cls).__new__(cls)
    # 各クラスが辞書(cls._instances)を持つようになるため、キーにクラスオブジェクトを含める必要がなくなる
    return cls._instances.setdefault((args, tuple(kwargs.items())), instance)


def flyweight(cls):
    # クラスオブジェクトの属性に代入した関数オブジェクトが
    # Python2: unbound method(=呼び出し時にバインドするインスタンスを指定する必要あり)
    # __new__はインスタンス生成時に暗黙的に呼び出されるが
    # 渡されるのはクラスオブジェクト(cls)であり、そのクラスのインスタンス(self)ではない
    # Python3: 関数オブジェクトのまま
    cls._instances = {}
    print(_get_instance)
    cls.__new__ = _get_instance
    print(cls.__new__)
    return cls

# Hoge = flyweight(Hoge)と同等
@flyweight
class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

# Piyo = flyweight(Piyo)と同等
@flyweight
class Piyo(object):
    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')
```