クラスメソッドでcacheを使うときの注意点

# まえがき
クラスのメソッドをmemoizeしたいとき、安易にlru_cacheを使ってはいけない

## 参考
https://qiita.com/fumitoh/items/f36dc494eec35a13f513

## 引用元
https://www.youtube.com/watch?v=sVjtp6tGo0g

# 実例

元のコードはこのような形
```python
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 秒かかる
```python
>>> c = C(4)
>>> c.compute(1)
computing...
4
>>> c.compute(1)
computing...
4
```

## ダメな例

```python
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 秒の待機は不要となる

```python
>>> c = C(4)
>>> c.compute(1)
computing...
4
>>> c.compute(1)
computing...
4
```

しかし、このときインスタンスを消去してもメモリを開放することができなくなる
```python
>>> 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
```

## 良い例（動画で紹介）
このような書き方によりメモリリークを回避できる
```python
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
```

# 良い例（今回提案する方法）
```python
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>
```

こちらの書き方でもメモリリークを回避できる

### メリット
1. docstring やIDEの補完を有効にすることができる
1. 即座にメモリ解放が行われる

### 気をつけるべきこと
次に示す例では `compute`関数が list のインスタンス変数 `self.yl` を利用する
このとき C_1 は動作するが、C_2 は動作しない
なぜなら C_2 では list をハッシュ化しようとするからだ

```python
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 はハッシュ化できないが、インスタンスはハッシュ化できる。
```python
>>> 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 のときも同様の注意が必要)
```python
>>> 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
```