Edited at

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

More than 3 years have passed since last update.

前回と同じく、こちらの記事を解釈した際のメモ。


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

原文ではMixinとしているが正直イマイチピンとこない。

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

コード自体は可変長引数を使ったfactoryの一般化と殆ど変わらない。違いは以下の点。


  • FlyweightFactoryが提供していた機能が継承によりHoge/Piyoクラスから使われている

  • ディクショナリのキーとして可変長引数だけでなくクラスも使う

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__メソッドをオーバーライドしてやらなくてはならない。

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パターンのロジックを定義した関数オブジェクト)を代入して返す。

# 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')