在Python中在运行时更改函数引用

5
我需要在运行时更改另一个函数内的调用函数。
考虑以下代码:
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

一个用户编写了代码 myfunc() ,它调用了全局定义的函数 now(),我不能编辑它。但是我想让它调用 Sim 实例的方法。因此,我需要在运行时“修补” myfunc()函数。
如何解决这个问题?
一个可能的解决方案是像这里所做的那样编辑字节码:http://web.archive.org/web/20140306210310/http://www.jonathon-vogel.com/posts/patching_function_bytecode_with_python,但我想知道是否有更简单的方法。

在您的实际用例中,nowmyfunc 是否在同一个模块中定义?您是否关心其他对 now 的使用会受到影响?如果它们在不同的模块中,myfunc 如何访问 now 函数?如果它执行的是 from somemodule import now ... now(),与 import somemodule ... somemodule.now(),所需的方法可能会有所不同。 - Weeble
是的,这很重要。我更新了示例以解决这个问题。最终用户将从库中导入全局的“now”和“Sim”。我可以修改全局的“now”,但我无法承受在全局“now”内进行昂贵的查找以定位本地版本的成本。 - jpcgt
5个回答

3
这比看上去复杂。要实现它,您需要:
  • 创建一个dict的子类,其中包含一个__missing__方法以保存now的新值。然后__missing__方法从通常的globals()字典中获取不在字典中的任何项。

  • 从现有的myfunc()函数创建一个新的函数对象,保留其代码对象,但使用您为全局变量创建的新字典。

  • 将新函数重新分配回全局变量,使用其函数名称。

以下是实现步骤:
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()

没有必要将它放在一个类中,但我仍然按照你最初的写法来做。

如果myfunc在另一个模块中,那么您需要重新编写Sim.patch()以将其补丁回该命名空间,例如module.myfunc = sim.patch(module.myfunc)


这段更新后的代码能否在两个不同的“Sim()”实例上工作?看起来它正在修改原始的“myfunc”,并且将修改之前的补丁。也许先深度复制“myfunc”,然后再打补丁可以解决问题? - jpcgt
应该能够与多个实例一起使用。显然,在全局范围内只能有一个版本的 myfunc,但是如果通过 Sim 实例调用它(通过添加 __call__ 方法使 Sim 可调用,或者仅通过调用 sim.func()),则每个实例都使用自己的 myfunc 版本。您还可以将修补程序推迟到调用实例时。 - kindall
这个方法很好用。我只想指出,globals()[funcname] = name 会改变原来的 myfunc,因此它应该是可选的,取决于所需的结果。我个人只是将其注释掉,现在达到了我想要的效果。 - jpcgt

1
这个答案仅供娱乐。请不要尝试此操作。
可以通过替换myfunc的包装函数来完成,该函数将全局变量字典复制一份,替换有问题的'now'值,使用原始myfunc的代码对象和新的全局字典创建一个新函数,然后调用该新函数而不是原始myfunc。像这样:
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()

然后:

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

有很多不做这件事的理由。如果myfunc访问的now函数不在与Sim相同的模块中,它将无法工作。它可能会破坏其他东西。此外,像Sim(myfunc)这样的普通类实例化以这种方式改变全局状态真的很邪恶。


1
你可以使用 mock 包,它已经处理了大部分补丁函数调用的繁重工作。在 Python 2(和早期版本的3?)中,这是一个第三方安装程序,但在 Python 3 标准库中成为 unittest.mock 的一部分。它主要用于测试,但你也可以尝试在这里使用它。
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

0

不是非常优雅,但你可以用另一个函数来包装你的函数,在函数运行时仅修改全局环境。

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()

-1

函数有一个func_globals属性。在这种情况下,您可以像这样实现patch

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)

这会打印出什么:

Hello World!
Hello Locals!

这将导致所有now的调用被替换,而不仅仅是从myfunc内部调用now的调用。 - BrenBarn
哎呀,错了。正在编辑我的答案。 - Ceasar
这会混淆你想要修补的函数和你想让另一个函数使用的函数。而且,它仍然会替换模块中其他函数对now的调用。 - user2357112
是的,你说得对。太匆忙了。当前的编辑应该解决OP的问题。 - Ceasar
您的编辑并没有解决问题。 myfunc.func_globals 是全局环境的引用,而不是它的副本。修改 myfunc.func_globals 具有与直接修改 globals() 结果相同的效果。 - BrenBarn
你说得对。在问题解决之前,我会将其保留下来以备后用。我不确定如何处理这个问题。 - Ceasar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接