LoginSignup
27

More than 5 years have passed since last update.

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

Posted at

monkey patchとは・・・

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

wiki参照

 題材

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

参考URL

↓↓こう書きたい↓↓

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は参照渡し)
  • 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で上書きする。
最終行置き換え.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関数は、 トップレベルで呼び出しを記載している。

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27