Pythonのクラスメンバ変数とメタクラス
とりあえず以下のようなプログラムについて考える.
class Reservoir(object): def get(self, key): value = key * 3 # Do something hard return value if __name__ == "__main__": obj = Reservoir() print(obj.get('KEY1')) # => KEY1KEY1KEY1 print(obj.get('KEY2')) # => KEY2KEY2KEY2 print(obj.get('KEY1')) # => KEY1KEY1KEY1
このget操作がコストを必要とする場合, キャッシュすることを考える. この場合, メンバ変数を使って以下のようにする.
import logging class Reservoir(object): def __init__(self): self.DATA = {} def get(self, key): if key in self.DATA.keys(): logging.info('The key [{}] is already processed.'.format(key)) return self.DATA[key] logging.info('Processing the key [{}].'.format(key)) value = key * 3 # Do something hard self.DATA[key] = value return value if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) obj = Reservoir() print(obj.get('KEY1')) # => INFO:root:Processing the key [KEY1]. # => KEY1KEY1KEY1 print(obj.get('KEY2')) # => INFO:root:Processing the key [KEY2]. # => KEY2KEY2KEY2 print(obj.get('KEY1')) # => INFO:root:The key [KEY1] is already processed. # => KEY1KEY1KEY1
ただし, これだとあくまでもインスタンス化されたオブジェクトに記憶されるだけなので他の場所で呼びだしても参照されない.
obj1 = Reservoir() print(obj1.get('KEY1')) # => INFO:root:Processing the key [KEY1]. # => KEY1KEY1KEY1 obj2 = Reservoir() print(obj2.get('KEY1')) # => INFO:root:Processing the key [KEY1]. # => KEY1KEY1KEY1
オブジェクトのメンバ変数ではなく、クラスのメンバ変数にしてやればインスタンス間で共有できる.
class Reservoir(object): DATA = {} def get(self, key): # same as above ... if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) print(Reservoir().get('KEY1')) # => INFO:root:Processing the key [KEY1]. # => KEY1KEY1KEY1 print(Reservoir().get('KEY1')) # => INFO:root:The key [KEY1] is already processed. # => KEY1KEY1KEY1
しかし次にこの手のクラスを継承して別のクラスを作ることを考える. 例えば以下のようにA, Bふたつの異なる処理をするクラスを考えると,
class Reservoir(object): DATA = {} def get(self, key): if key in self.DATA.keys(): return self.DATA[key] value = self.process(key) self.DATA[key] = value return value def process(self, key): raise NotImplementedError() class A(Reservoir): def process(self, key): return 'A processed the key, {}.'.format(key) class B(Reservoir): def process(self, key): return 'B processed the key, {}.'.format(key) if __name__ == "__main__": print(A().get('KEY1')) # => A processed the key [KEY1]. print(B().get('KEY1')) # => A processed the key [KEY1].
同じクラスを継承しているため, 異なるクラス間で結果を共有してしまう. これはこれで正しい挙動だが, 今回の場合, AとBの結果は別々に記憶しておきたい.
そこでメタクラスを使う.
import logging class ReservoirMeta(type): def __new__(cls, name, bases, attrs): attrs['DATA'] = {} def _get(self, key): if key in self.DATA.keys(): logging.info("A cache for the key [{}] works.".format(key)) return self.DATA[key] value = self.process(key) self.DATA[key] = value return value attrs['get'] = _get if 'process' not in attrs.keys(): def _process(self, key): raise NotImplementedError() attrs['process'] = _process return super().__new__(cls, name, bases, attrs) class A(metaclass=ReservoirMeta): def process(self, key): return 'A processed the key [{}].'.format(key) class B(metaclass=ReservoirMeta): def process(self, key): return 'B processed the key [{}].'.format(key) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) print(A().get('KEY1')) # => A processed the key [KEY1]. print(B().get('KEY1')) # => B processed the key [KEY1]. print(A().get('KEY1')) # => INFO:root:A cache for the key [KEY1] works. # => A processed the key [KEY1]. print(B().get('KEY1')) # => INFO:root:A cache for the key [KEY1] works. # => B processed the key [KEY1].
ReservoirMetaはReservoir相当の機能を備えたテンプレートとなってAやBを生成する. 言わば後付けでReservoirMetaの機能がクラスに付与される. というわけで継承とは異なる. 実際,
print(A.__bases__) # => (<class 'object'>,)
ただ後付けしているだけなのでデコレータでもいける. というか, 意味合い的に同じものだと考えて良いのかもしれない.
def reservoir(cls): setattr(cls, 'DATA', {}) def _get(self, key): if key in self.DATA.keys(): logging.info("A cache for the key [{}] works.".format(key)) return self.DATA[key] value = self.process(key) self.DATA[key] = value return value setattr(cls, 'get', _get) if not hasattr(cls, 'process'): def _process(self, key): raise NotImplementedError() setattr(cls, 'process', _process) return cls @reservoir class A: # same as above ...
というわけでメタクラスの感覚をつかむために, ここまでさんざんやってきたがデコレータに戻ってきてしまった. というか, この事例ではメンバ関数にデコレータを直接かませば十分である.
import logging def reservoir_deco(func): DATA = {} def wrapped(self, key): if key in DATA.keys(): logging.info("A cache for the key [{}] works.".format(key)) return DATA[key] value = func(self, key) DATA[key] = value return value return wrapped class A(object): @reservoir_deco def get(self, key): return 'A processed the key [{}].'.format(key) class B(object): @reservoir_deco def get(self, key): return 'B processed the key [{}].'.format(key) if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) print(A().get('KEY1')) # => A processed the key [KEY1]. print(B().get('KEY1')) # => B processed the key [KEY1]. print(A().get('KEY1')) # => INFO:root:A cache for the key [KEY1] works. # => A processed the key [KEY1]. print(B().get('KEY1')) # => INFO:root:A cache for the key [KEY1] works. # => B processed the key [KEY1].