Posted at

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

More than 3 years have passed since last update.

横の横の人が教えてくれた記事を解釈したメモ。


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)