Pythonで一部分のみ安全に?monkey patchしたい

# monkey patchとは・・・

> モンキーパッチは、オリジナルのソースコードを変更することなく、実行時に動的言語(例えばSmalltalk, JavaScript, Objective-C, Ruby, Perl, Python, Groovy, など)のコードを拡張したり、変更したりする方法である。

[wiki参照](https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%B3%E3%82%AD%E3%83%BC%E3%83%91%E3%83%83%E3%83%81)

#　題材 
Python3でroundが四捨五入じゃなくなったんだって

```py
print round(2.5) # => 3.0
```
```py3
print(round(2.5)) # => 2
```

python3からround関数は、浮動小数点として扱うようになったかららしい。
これを四捨五入で使えるように!!

# 四捨五入のコード

```py3
def custom_round(x, d=0):
    import math
    p = 10 ** d
    return float(math.floor((x * p) + math.copysign(0.5, x)))/p
print(custom_round(2.5)) #=> 3.0
```
[参考URL](http://python3porting.com/differences.html#rounding-behavior)

# ↓↓こう書きたい↓↓

```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
```
# setting.pyの生成
## 1.とりあえずmonkey patch
```py3:setting.py
import builtins # 組み込み関数はbuiltinsに入っている
def custom_round(x, d=0):
    import math
    p = 10 ** d
    return float(math.floor((x * p) + math.copysign(0.5, x)))/p
builtins.round = custom_round # monkey patch
```
* custom関数内で対象関数を呼ぶと、無限ループする。
  * => cuntom関数外でfrom builtins import round as aliasとし、alias関数で呼び出せる。
    (asにより関数はobject化されている為。moduleでは、asは参照渡し)
* round = custom_roundでは、roundはすでにobject化されている為、monkey patchにならない。

```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
```
###### 表示されたから成功・・・と思いきや
```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
```
```py3:sample2.py
print(round(2.5)) #=> 3.0
```
* 別ファイルにて、対象関数を実行すると、custom関数が呼ばれる。
   * => libraryなど外部moduleで対象関数が使用されていると、破壊行動になってしまう可能性がある。
   
###### => 安全じゃない!!!!? 

## 2.stack traceから呼び出し元のname spaceをhook
```py3:
import inspect, imp
import builtins
def custom_round(x, d=0):
    import math
    p = 10 ** d
    return float(math.floor((x * p) + math.copysign(0.5, x)))/p

frame = [frame for (frame, filename, _, _, _,_) in 
             inspect.getouterframes(inspect.currentframe())[1:] 
                 if not 'importlib' in filename and not __file__ in filename][0]
        # 呼び出しもとの取得、importには、importlibを介している場合がある為
frame.f_locals['round'] = custom_round
```
```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
```
```py3:sample2.py
print(round(2.5)) #=> 2
```
* importのtraceにimportlibが入ることがある。(階層を持つときに確認)
* builtins(組み込み関数)では、moduleを介せずnamespaceに格納される。
  * moduleでは、最終行を下記のコードに置き換える。
     * namespaceのmoduleを対象関数のみを置き換えたmoduleで上書きする。
  
```py3:最終行置き換え.py
# ${module} : 対象module, ${function} : 対象関数
replacing = imp.load_module('temp', *imp.find_module(${module}))
setattr(replacing, '${function}', ${custom関数})
frame.f_locals[${module}] = replacing
```

###### Fileを切り分けれたかと思いきや・・・sample2ではsetting処理されない・・・
```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
```
```py3:sample2.py
import setting
print(round(2.5)) #=> 2
```
=> 1度importしたmoduleは、sys.modulesに保持され、2度目のimportでは、sys.moduleから参照し、codeは呼ばれない為。

## 3.builtins.importを直接置き換え、import時に、monkey patch関数を呼ぶ。
```py3:setting.py
import inspect, imp
import builtins
def custom_round(x, d=0):
    import math
    p = 10 ** d
    return float(math.floor((x * p) + math.copysign(0.5, x)))/p

def hooking():
    frame = [frame for (frame, filename, _, _, _,_) in 
             inspect.getouterframes(inspect.currentframe())[1:] 
                 if not 'importlib' in filename and not __file__ in filename][0]
        # 呼び出しもとの取得、importには、importlibを介している場合がある為
    frame.f_locals['round'] = custom_round

class Importer(object):
    old_import = __import__
    def new_import(self, *args, **kwargs):
        if args[0] == __name__: hooking() 
        return self.old_import(*args, **kwargs)

hooking()
import builtins
builtins.__import__ = Importer().new_import
```
```py3:sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
```
```py3:sample2.py
import setting
print(round(2.5)) #=> 3.0
```
* importをhookする事によって、monkey patch関数を呼ぶ。
  * python3では、meta path, path hooksなどimport hooksの機構が存在するが、
    sys.modulesに保持されているmoduleでは、これらは呼ばれない。
  * 1度目のimportにて、importのhookを追加するため、1度目のmonkey pathc関数は、
    トップレベルで呼び出しを記載している。

# まとめ

最終的に、import settingが書かれているmoduleのみでのmonkey patchを実装できました。