上書きされないモンキーパッチ

#はじめに

モンキーパッチをしたつもりが、メソッドの上書きが適切にできていないという事象にハマりました。最終的にハマった原因と解決策がわかったので、誰かの役に立てばと思い記事にします。

#モンキーパッチできない原因
私の場合は、独立した2つの原因によって適切な上書きができていませんでした。

- 原因1：importしたときに新しくできるメソッドのメモリが、上書きするメソッドのメモリと異なってしまう。詳しくは以前書いた記事[飛び越えモンキーパッチ](https://qiita.com/StoneRIeverKS/items/c9478be7b12559752ae5)を参考にしてください、
- 原因2：\_\_init\_\_.pyによって意図せずにメソッドがimportされてしまう。それにより、モンキーパッチしたあともする前のメソッドが呼び出されてしまい、狙った動作をしない。本記事はこちらの説明になります。

#ハマったこと
まずは、発生した事象を再現するためにディレクトリの構造とスクリプトを示します。

```text
./
├ main.py
└─ parent/
   ├ __init__.py
   ├ module1.py
   └─ module2.py
```

```Python:__init__.py
from .module1 import func_a
from .module2 import func_b
```
```Python:module1.py
def func_a(i):
    return i
```

```Python:module2.py
from .module1 import func_a

def func_b(i):
    return func_a(i)
```
このような設定で、下記のmain.pyを実行します。

```Python:main.py
import parent

print(parent.func_a(1))
#出力結果：1
print(parent.func_b(1))
#出力結果：1
```
これは予想通りだと思います。なお、main.pyで

```Python
import parent.module1 #module1のimport
import parent.module2 #module2のimport
```

をしなくても、module1のfunc_aとmodule2のfunc_bが使用できるのは、

```Python
import parent
```
をしたときに、parent内の\_\_init__.pyによってmodule1とmodule2がimportされるからです。

さて、ここでmain.pyを書き換えてメソッドfunc_aにモンキーパッチしてみます。

```Python:main_monkey.py
import parent #parentのimport

#上書き用メソッド
def func_a_alt(i):
    return 10*i

#モンキーパッチ!
parent.module1.func_a = func_a_alt

#モンキーパッチしたあとに再度module2をimportし、上書きされたfunc_aをfunc_bに反映させる
import parent.module2

#このときの各メソッドの出力結果
print(parent.module1.func_a(1))
#出力結果：10

print(parent.module2.func_b(1))
'''
出力結果の予想:func_aを上書きした後にparent.module2を
再度importしたからfunc_bも上書きされたメソッドで出力されるはず！
実際の出力結果:：1←うまく上書きできていない！
'''
```
となります。[飛び越えモンキーパッチ](https://qiita.com/StoneRIeverKS/items/c9478be7b12559752ae5)に記載したように

- おおもとのメソッドを上書きする
- 他の.pyからメソッドを呼び出す前に上書きする

を両方実行したのに上書きできていません。なぜでしょうか…？

#原因
原因は下記の2点です。

- \_\_init__.pyが最初にparent配下のmodule1、module2をimportしてしまうこと
- 一度importされたモジュールはメモリ上に保存され、2回目以降のimportはこのメモリから読み込まれること

具体的にスクリプトをコードを追って何が行われているのか見ていきましょう。

```Python:main_monkey(解説つき).py
import parent 
#parent配下の__init__.pyによってmodule1.py,module2.pyが作業メモリにロードされる。---(1)

def func_a_alt(i):
    return 10*i

#モンキーパッチ!
parent.module1.func_a = func_a_alt #作業メモリ上のmodule1.pyのfunc_aが上書きされる。

#モンキーパッチしたあとに再度module2をimportし、上書きされたfunc_aをfunc_bに反映させる←ここが間違い！
import parent.module2 
'''
作業メモリ上のmodule2.pyにアクセスする。
このとき、あくまで作業メモリ上のmodule2.pyにアクセスするだけなので、
(1)でロードされたmodule2.pyにアクセスだけなので、モンキーパッチされたfunc_aを読み込まない。---(2)
'''
#このときの各メソッドの出力結果
print(parent.module1.func_a(1))
#出力結果：10

print(parent.module2.func_b(1))
#モンキーパッチ後のfunc_aが読み込まれていないため((2)の説明参照)、「1」が表示される。
```

上のスクリプトを見ると(2)の部分でモンキーパッチ後のfunc_aがfunc_bに反映されていないことがわかりました。
逆にいえばここをクリアすれば、狙った通りのモンキーパッチができます。

#解決策
問題だったのは、importは2回目以降は作業メモリにしかアクセスしないことでした。したがって、ここでもう一度ディスクから呼び出し、module2.pyを実行する必要があります。そこでスクリプトを以下のように変更します。

```Python:main_solution.py
import parent 
#parent配下の__init__.pyによってmodule1.py,module2.pyが作業メモリにロードされる。

def func_a_alt(i):
    return 10*i

#モンキーパッチ!
parent.module1.func_a = func_a_alt #作業メモリ上のmodule1.pyのfunc_aが上書きされる。

'''
修正ポイント！
module2を再度ロードする
'''
from importlib import reload
reload(parent.module2)

#このときの各メソッドの出力結果
print(parent.module1.func_a(1))
#出力結果：10

print(parent.module2.func_b(1))
#出力結果：10
```

ポイントは、module2をimportするのではなく、reloadすることです。なお、reloadするところで誤って

```Python
from importlib import reload
reload(parent)
```
とすると、parent配下の\_\_init__.pyが再度実行されて、モンキーパッチする前のfunc_aが呼び出されるので注意してください。

#まとめ
今回は\_\_init__.pyがあったときにモンキーパッチする方法について説明しました。
ポイントは、<strong>モンキーパッチ後に対象のモジュールを再度リロードすること</strong>です。
何かのお役に立てれば幸いです。


#参考文献
この記事を書くのに参考にした記事を下に貼っておきます。


- [Python の __init__.py とは何なのか](https://qiita.com/msi/items/d91ea3900373ff8b09d7)
- [Pythonでモジュールをリロードする方法【初心者向け】](https://techacademy.jp/magazine/19196)
- [飛び越えモンキーパッチ](https://qiita.com/StoneRIeverKS/items/c9478be7b12559752ae5)