LoginSignup
3
3

More than 5 years have passed since last update.

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

Posted at

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

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)

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3