monkey patchとは・・・
モンキーパッチは、オリジナルのソースコードを変更することなく、実行時に動的言語(例えばSmalltalk, JavaScript, Objective-C, Ruby, Perl, Python, Groovy, など)のコードを拡張したり、変更したりする方法である。
# 題材
Python3でroundが四捨五入じゃなくなったんだって
print round(2.5) # => 3.0
print(round(2.5)) # => 2
python3からround関数は、浮動小数点として扱うようになったかららしい。
これを四捨五入で使えるように!!
四捨五入のコード
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
↓↓こう書きたい↓↓
sample.py
import setting
print(round(2.5)) #=> 3.0
setting.pyの生成
1.とりあえずmonkey patch
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は参照渡し)
- => cuntom関数外でfrom builtins import round as aliasとし、alias関数で呼び出せる。
- round = custom_roundでは、roundはすでにobject化されている為、monkey patchにならない。
sample.py
import setting
print(round(2.5)) #=> 3.0
表示されたから成功・・・と思いきや
sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
sample2.py
print(round(2.5)) #=> 3.0
- 別ファイルにて、対象関数を実行すると、custom関数が呼ばれる。
- => libraryなど外部moduleで対象関数が使用されていると、破壊行動になってしまう可能性がある。
=> 安全じゃない!!!!?
2.stack traceから呼び出し元のname spaceをhook
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
sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
sample2.py
print(round(2.5)) #=> 2
- importのtraceにimportlibが入ることがある。(階層を持つときに確認)
- builtins(組み込み関数)では、moduleを介せずnamespaceに格納される。
- moduleでは、最終行を下記のコードに置き換える。
- namespaceのmoduleを対象関数のみを置き換えたmoduleで上書きする。
- moduleでは、最終行を下記のコードに置き換える。
最終行置き換え.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処理されない・・・
sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
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関数を呼ぶ。
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
sample.py
import setting
print(round(2.5)) #=> 3.0
import sample2
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関数は、
トップレベルで呼び出しを記載している。
- python3では、meta path, path hooksなどimport hooksの機構が存在するが、
まとめ
最終的に、import settingが書かれているmoduleのみでのmonkey patchを実装できました。