継承を用いた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')