Change reference to function in run-time in Python

大兔子大兔子 提交于 2019-12-21 17:24:50

问题


I need to change a call to a function inside another function during run-time.

Consider the following code:

def now():
    print "Hello World!"

class Sim:
    def __init__(self, arg, msg):
        self.msg = msg
        self.func = arg
        self.patch(self.func)

    def now(self):
        print self.msg

    def run(self):
        self.func()

    def patch(self, func):
        # Any references to the global now() in func
        # are replaced with the self.now() method.

def myfunc():
    now()

Then ...

>>> a = Sim(myfunc, "Hello Locals #1")
>>> b = Sim(myfunc, "Hello Locals #2")
>>> b.run()
Hello Locals #2
>>> a.run()
Hello Locals #1

One user has written code, myfunc(), that makes a call to a globally defined function now() and I cannot edit it. But I want it to call the method of the Sim instance instead. So I would need to "patch" the myfunc() function in run-time.

How could I go about this?

One possible solution is to edit the bytecode as done here: http://web.archive.org/web/20140306210310/http://www.jonathon-vogel.com/posts/patching_function_bytecode_with_python but I'm wondering if there is an easier way.


回答1:


This is trickier than it might seem. To do it you need to:

  • Create a dict subclass with a __missing__ method to hold your new value of now. The __missing__ method then grabs any items not in the dictionary from the usual globals() dict.

  • Create a new function object from the existing myfunc() function, keeping its code object but using the new dictionary you created for globals.

  • Assign the new function back into globals using its function name.

Here's how:

def now():
    print "Hello World!"

class NowGlobals(dict):
    def __init__(self, now, globals):
        self["now"] = now
        self.globals = globals
    def __missing__(self, key):
        return self.globals[key]

class Sim(object):
    def __init__(self, func):
        func = self.patch(func)
        self.func = func
    def now(self):
        print "Hello locals!"
    def patch(self, func):
        funcname   = func.__name__
        nowglobals = NowGlobals(self.now, func.func_globals)
        func = type(func)(func.func_code, nowglobals)
        globals()[funcname] = func
        return func

def myfunc():
    now()

sim = Sim(myfunc)
myfunc()

There is really no need to have it in a class, but I've kept it that way since that's the way you wrote it originally.

If myfunc is in another module, you'll need to rewrite Sim.patch() to patch back into that namespace, e.g. module.myfunc = sim.patch(module.myfunc)




回答2:


This answer is intended for amusement only. Please don't ever do this.

It can be done by replacing myfunc with a wrapper function which makes a copy of the global variable dictionary, replaces the offending 'now' value, makes a new function with the code object of the original myfunc and the new global dictionary, and then calls that new function instead of the original myfunc. Like this:

import types
def now():
    print("Hello World!")

class Sim:
    def __init__(self, arg):
        self.func = arg
        self.patch(self.func)
        self.func()

    def now(self):
        print("Hello Locals!")

    def patch(self, func):

        def wrapper(*args, **kw):
            globalcopy = globals().copy()
            globalcopy['now'] = self.now
            newfunc = types.FunctionType(func.__code__, globalcopy, func.__name__)
            return newfunc(*args, **kw)
        globals()[func.__name__] = wrapper

def myfunc():
    now()

def otherfunc():
    now()

Then:

>>> myfunc()
Hello World!
>>> otherfunc()
Hello World!
>>> Sim(myfunc)
Hello World!
<__main__.Sim instance at 0x0000000002AD96C8>
>>> myfunc()
Hello Locals!
>>> otherfunc()
Hello World!

There are lots of reasons not to do this. It won't work if the now function that myfunc accesses is not in the same module as Sim. It might break other things. Also, having a plain class instantiation like Sim(myfunc) change global state in this way is really evil.




回答3:


Not very elegant, but you can wrap your func with another function that modifies the global environment only while func is running.

def now():
    print "Hello World!"

class Sim:
    def __init__(self, arg, msg):
        self.msg = msg
    self.func = arg
    self.patch(self.func)

  def now(self):
    print self.msg

  def run(self):
    self.func()

  def patch(self, func):
    # Any references to the global now() in func
    # are replaced with the self.now() method.

    def newfunc():
        global now
        tmp = now
        now = self.now
        func()
        now = tmp

    self.func = newfunc


def myfunc():
    now()



回答4:


You can use the mock package, which already takes care of much of the hard work in patching a function call. It's a third-party install in Python 2 (and early versions of 3?), but part of the Python 3 standard library as unittest.mock. It's mainly intended for testing, but you could try using it here.

import mock

def now():
    print "Hello World!"

class Sim:
    # Your code from above here

def myfunc():
    now()

myfunc()   # Outputs Hello World!

s = Sim(myfunc, "Hello Locals #1")
mock.patch('__main__.now', wraps=s.now).start()

myfunc()   # Now outputs Hello Locals #1



回答5:


Functions have a func_globals attribute. In this case you implement patch like so:

def now():
    print "Hello World!"


class Sim:
    def __init__(self, arg):
        self.func = arg
        self.patch(self.func)
        self.func()

    def now(self):
        print "Hello Locals!"

    def patch(self, func):
        # Any references to the global now() in func
        # are replaced with the self.now() method.
        func.func_globals['now'] = self.now


def myfunc():
    now()


Sim(myfunc)

Which prints:

Hello World!
Hello Locals!


来源:https://stackoverflow.com/questions/30061421/change-reference-to-function-in-run-time-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!