まえがき
クラスのメソッドをmemoizeしたいとき、安易にlru_cacheを使ってはいけない
参考
引用元
実例
元のコードはこのような形
import time
class C:
def __init__(self, y: int) -> None:
self.y = y
def compute(self, x: int) -> int:
print('computing...')
time.sleep(.5)
return self.y * x
def __del__(self) -> None:
print(f'delete {self}')
compute
が呼び出されるたびに計算が走り 0.5 秒かかる
>>> c = C(4)
>>> c.compute(1)
computing...
4
>>> c.compute(1)
computing...
4
ダメな例
class C_bad:
def __init__(self, y: int) -> None:
self.y = y
@functools.lru_cache(maxsize=128)
def compute(self, x: int) -> int:
print('computing...')
time.sleep(.5)
return self.y * x
def __del__(self) -> None:
print(f'delete {self}')
このように書くことで compute
の結果がキャッシュ化され 0.5 秒の待機は不要となる
>>> c = C(4)
>>> c.compute(1)
computing...
4
>>> c.compute(1)
computing...
4
しかし、このときインスタンスを消去してもメモリを開放することができなくなる
>>> c = None
delete <__main__.C object at 0x000001C7D7FDB790>
>>> c_bad = C_bad(4)
>>> c_bad.compute(1)
computing...
4
>>> c_bad.compute(1)
4
>>> c_bad = None
良い例(動画で紹介)
このような書き方によりメモリリークを回避できる
class C_1:
def __init__(self, y: int) -> None:
self.y = y
self.compute = functools.lru_cache(maxsize=128)(self._compute)
def _compute(self, x: int) -> int:
print('computing...')
time.sleep(.5)
return self.y * x
def __del__(self) -> None:
print(f'delete {self}')
>>> s = C_1(4)
>>> s.compute(4)
computing...
16
>>> s.compute(4)
16
>>> del s
>>> import gc
>>> gc.collect()
delete <__main__.C_1 object at 0x0000023202ADB550>
36
良い例(今回提案する方法)
class C_2:
def __init__(self, y: int) -> None:
self.y = y
def compute(self, x: int) -> int:
return self._compute(self.y, x)
@staticmethod
@functools.lru_cache(maxsize=128)
def _compute(y: int, x: int) -> int:
print('computing...')
time.sleep(.5)
return y * x
def __del__(self) -> None:
print(f'delete {self}')
>>> g = C_2(4)
>>> g.compute(1)
computing...
4
>>> g.compute(1)
4
>>> del g
delete <__main__.C_2 object at 0x0000023202DF96D0>
こちらの書き方でもメモリリークを回避できる
メリット
- docstring やIDEの補完を有効にすることができる
- 即座にメモリ解放が行われる
気をつけるべきこと
次に示す例では compute
関数が list のインスタンス変数 self.yl
を利用する
このとき C_1 は動作するが、C_2 は動作しない
なぜなら C_2 では list をハッシュ化しようとするからだ
class C_1:
def __init__(self, yl: list) -> None:
self.yl = yl
self.compute = functools.lru_cache(maxsize=128)(self._compute)
def _compute(self, x: int) -> int:
print('computing...')
time.sleep(.5)
return sum(self.yl) * x
def __del__(self) -> None:
print(f'delete {self}')
class C_2:
def __init__(self, yl: list) -> None:
self.yl = yl
def compute(self, x: int) -> int:
return self._compute(self.yl, x)
@staticmethod
@functools.lru_cache(maxsize=128)
def _compute(yl: list, x: int) -> int:
print('computing...')
time.sleep(.5)
return yl * x
def __del__(self) -> None:
print(f'delete {self}')
>>> c = C_1([2,1])
>>> c.compute(1)
computing...
3
>>> c.compute(1)
3
>>> c.compute(1)
3
>>> import gc
>>> gc.collect()
delete <__main__.C_1 object at 0x000001BB7894B550>
37
>>> c = C_2([2,1])
>>> c.compute(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "xxxxxx\method_cache2.py", line 34, in compute
return self._compute(self.yl, x)
^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
下の例にあるように、list はハッシュ化できないが、インスタンスはハッシュ化できる。
>>> c = C_1([2,1])
>>> hash(c)
119043543925
>>> hash(c.yl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
一方で、インスタンスのハッシュ値は変数を変更しても変わらないため
そのことに注意して実装する必要がある
下の例では途中で self.yl
の値を変更したが、それ以前に compute
のキャッシュがあるとそれが出力され、ただしい結果が得られない
キャッシュをクリアすると正しい結果となる
(最初の例である self.y
が int のときも同様の注意が必要)
>>> c.compute(2)
computing...
6
>>> c.yl = [1, 2, 3]
>>> c.compute(2)
6
>>> hash(c)
119043543925
>>> c.compute.cache_clear()
>>> c.compute(2)
computing...
12