横の横の人が教えてくれた記事を解釈したメモ。
GoFに忠実なFlyweightパターン
1つの対象に対して1つのファクトリが定義される。
以下のコードは元の記事とほとんど同じだが、名前付き引数を与えるように変更を加えている。
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
class HogeFactory(object):
def __init__(self):
self._instances = {}
def get_instance(self, args1, kwargs1):
if ((args1), (kwargs1)) not in self._instances:
self._instances[((args1), (kwargs1))] = Hoge(args1, kwargs1=kwargs1)
return self._instances[((args1), (kwargs1))]
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
class PiyoFactory(object):
def __init__(self):
self._instances = {}
def get_instance(self, args1, kwargs1='test1', kwargs2='test2'):
if ((args1), (kwargs1, kwargs2)) not in self._instances:
self._instances[((args1), (kwargs1, kwargs2))] = Piyo(args1, kwargs1=kwargs1, kwargs2=kwargs2)
return self._instances[((args1), (kwargs1, kwargs2))]
hogeFactory = HogeFactory()
piyoFactory = PiyoFactory()
assert hogeFactory.get_instance(1, kwargs1=2) is hogeFactory.get_instance(1, kwargs1=2)
assert hogeFactory.get_instance(1, kwargs1=2) is not hogeFactory.get_instance(1, kwargs1=3)
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is piyoFactory.get_instance('a', kwargs1='b', kwargs2='c')
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is not piyoFactory.get_instance('a', kwargs1='b', kwargs2='d')
可変長引数を使ったFactoryの一般化
なぜ1つの対象に対して1つのファクトリが必要だったかといえば、各対象のコンストラクタのシグネチャが異なる=get_instance
メソッドのシグネチャが異なるためだった。
Pythonの可変長引数を使えばシグネチャの違いを吸収することができる。
class FlyweightFactory(object):
def __init__(self, cls):
self._instances = {}
self._cls = cls
def get_instance(self, *args, **kwargs):
return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
hogeFactory = FlyweightFactory(Hoge)
piyoFactory = FlyweightFactory(Piyo)
assert hogeFactory.get_instance(1, kwargs1=2) is hogeFactory.get_instance(1, kwargs1=2)
assert hogeFactory.get_instance(1, kwargs1=2) is not hogeFactory.get_instance(1, kwargs1=3)
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is piyoFactory.get_instance('a', kwargs1='b', kwargs2='c')
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is not piyoFactory.get_instance('a', kwargs1='b', kwargs2='d')
Factoryのデコレータ化(クラス)
前述のFlyweightFactory
をデコレータ化すると更にシンプルに書ける。
class Flyweight(object):
def __init__(self, cls):
self._instances = {}
self._cls = cls
def __call__(self, *args, **kwargs):
return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))
@Flyweight
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
@Flyweight
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
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', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')
デコレータはつけた対象を引数にとり呼び出される。この処理と等価なコードをデコレータを使用せずに書くと以下のようになる。
get_instance
を呼び出すかわりに、Flyweightインスタンスを関数として読んでいる違いはあるが、一つ前のスニペットとそう違いがないことが分かる。
(ref. http://docs.python.jp/3.4/reference/datamodel.html#object.__call__)
class Flyweight(object):
def __init__(self, cls):
self._instances = {}
self._cls = cls
def __call__(self, *args, **kwargs):
return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
# Hoge,Piyoを引数に取り, Flyweightインスタンスを生成する。
Hoge = Flyweight(Hoge)
Piyo = Flyweight(Piyo)
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', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')
Factoryのデコレータ化(クロージャ)
更に言うとcallableでありさえすれば良いのでクラスである必要もない、というのが以下のクロージャを利用した実装。
def flyweight(cls):
instances = {}
return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))
@flyweight
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
@flyweight
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
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', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')
デコレータを使わない等価なコードは以下のようになる。
def flyweight(cls):
instances = {}
return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
Hoge = flyweight(Hoge)
Piyo = flyweight(Piyo)
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', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')
このやり方は非常にスマートであるが、「Hoge
/Piyo
クラスのサブクラスを作れない」という欠点がある。
これはデコレータを使わないコードを見ると分かるが、Hoge
/Piyo
がもはやクラスではなく関数オブジェクトになっているためだ。従って以下のコードはエラーになる。
def flyweight(cls):
instances = {}
return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))
@flyweight
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
@flyweight
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
# Hogeはもはやクラスではないため継承不可
class Hoge2(Hoge):
def __init__(self, args1, kwargs1='test1'):
super().__init__(args1, kwargs1=kwargs1)
デコレータによりHoge
/Piyo
がクラスではなくなる、という意味ではデコレータをクラスで表現した場合も変わらないが、あちらはHoge
/Piyo
クラスを置き変えたFlyweight
クラスのインスタンスが元のクラスを保持しているのがミソ。
そのため、以下のようなコードでサブクラス化できる。
class Flyweight(object):
def __init__(self, cls):
self._instances = {}
self._cls = cls
def __call__(self, *args, **kwargs):
return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))
@Flyweight
class Hoge(object):
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1
@Flyweight
class Piyo(object):
def __init__(self, args1, kwargs1, kwargs2):
self.args1 = args1
self.kwargs1 = kwargs1
self.kwargs2 = kwargs2
# Hoge(実体はFlyweightインスタンス)は元のクラスを保持するインスタンス変数_clsを持つ
class Hoge2(Hoge._cls):
def __init__(self, args1, kwargs1='test1'):
super().__init__(args1, kwargs1=kwargs1)