Edited at

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

More than 3 years have passed since last update.

前前回前回に引き続き、こちらの記事を解釈した際のメモ。


メタクラスを用いたFlyweightパターン(typeクラスのサブクラス)

メタクラスを用いた方法の前に前提となるtypeクラスについて触れておく。typeクラスはクラス定義(=クラスオブジェクト)を生成する際に用いられる。

def methodA(self):

print('methodA')

# クラス名、親クラス(複数指定可)、名前空間(辞書により表現する、ここでは'methodA'にmethodA関数を紐付けている)を引数に取り
# クラスオブジェクトを生成する
ClassA = type('ClassA', (object,), {'methodA': methodA})
a = ClassA()
a.methodA()

# 以下の定義と同等
# class ClassA(object):
# def methodA(self):
# print('methodA')

前述の通り、typeはクラスである。そのためサブクラスを作成することができる。

サブクラスはtypeクラスの代わりにクラスオブジェクト生成に使用でき、__new__メソッドや__init__メソッドをオーバーライドすることで、クラスオブジェクト生成時に処理を追加できる。

def methodA(self):

print('methodA')

class Meta(type):
def __new__(cls, name, bases, namespace):
# クラスオブジェクト生成時に追加の処理を差し込める
print('hoge')
return super().__new__(cls, name, bases, namespace)

# typeクラスの代わりにMetaクラス(=typeクラスのサブクラス)を使用できる
ClassA = Meta('ClassA', (object,), {'methodA': methodA})
a = ClassA()
a.methodA()

通常のクラス定義構文でクラスオブジェクト生成時にtypeクラス以外を使用したい場合、Python3ではmetaclassキーワードを使用する。この時に指定するクラスをメタクラスと呼ぶ。

class Meta(type):

def __new__(cls, name, bases, namespace):
# クラスオブジェクト生成時に追加の処理を差し込める
print('hoge')
return super().__new__(cls, name, bases, namespace)

class ClassA(object, metaclass=Meta):
# Python2のメタクラスの指定
#class ClassA(object):
# __metaclass__ =Meta
def methodA(self):
print('methodA')

a = ClassA()
a.methodA()

以下は、「クラスオブジェクト生成時に処理を追加できる」というメタクラスの性質を利用して、Flyweightパターンのロジックをクラスオジェクトの生成時に差し込む例である。

class MetaFlyweight(type):

def __new__(cls, name, bases, namespace):
# typeクラスの__new__メソッドはクラスオブジェクトを返す
clsobj = type.__new__(cls, name, bases, namespace)

clsobj._instances = {}
def _get_instance(cls, *args, **kargs):
instance = super(cls, cls).__new__(cls)
return cls._instances.setdefault(
(args, tuple(kargs.items())), instance)
# staticメソッドにしなくても動くが、以下の記述に従いstaticメソッドにしておく
# (ref.) https://docs.python.org/3.3/reference/datamodel.html#object.__new__
clsobj.__new__ = staticmethod(_get_instance)
return clsobj

class Hoge(object, metaclass=MetaFlyweight):
# Python2のメタクラスの指定
#class Hoge(object):
# __metaclass__ = MetaFlyweight
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1

class Piyo(object, metaclass=MetaFlyweight):
# Python2のメタクラスの指定
#class Piyo(object):
# __metaclass__ = MetaFlyweight
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')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')


メタクラスを用いたFlyweightパターン(関数)

しかし必ずtypeクラスのサブクラスを生成する必要はない。

クラスオブジェクト生成時には、「クラス名(文字列)」「親クラス(タプル)」「名前空間(辞書)」を与えたtypeクラスが呼び出される。ただしここで必要なのはtypeクラス(もしくはサブクラス)というよりも、「上記3つの引数を受け取るcallableなオブジェクト」なのだ。そのため、以下のように関数をメタクラスに指定することも可能である。

def meta_flyweight(name, bases, namespace):

clsobj = type(name, bases, namespace)

clsobj._instances = {}
def _get_instance(cls, *args, **kargs):
instance = super(cls, cls).__new__(cls)
return cls._instances.setdefault(
(args, tuple(kargs.items())), instance)
# staticメソッドにしなくても動くが、以下の記述に従いstaticメソッドにしておく
# (ref.) https://docs.python.org/3.3/reference/datamodel.html#object.__new__
clsobj.__new__ = staticmethod(_get_instance)
return clsobj

class Hoge(object, metaclass=meta_flyweight):
# Python2のメタクラスの指定
#class Hoge(object):
# __metaclass__ = meta_flyweight
def __init__(self, args1, kwargs1='test1'):
self.args1 = args1
self.kwargs1 = kwargs1

class Piyo(object, metaclass=meta_flyweight):
# Python2のメタクラスの指定
#class Piyo(object):
# __metaclass__ = meta_flyweight
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')
assert Hoge('a', kwargs1='b') is not Piyo('a', kwargs1='b')

*元の記事では更にlamdaを用いた例を紹介しているが、これはさすがにトリッキーなので省く。