【问题标题】:How does one monkey patch a function in python?一只猴子如何在 python 中修补一个函数?
【发布时间】:2025-12-07 15:50:01
【问题描述】:

我在用另一个函数替换来自不同模块的函数时遇到问题,这让我发疯。

假设我有一个如下所示的模块 bar.py:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

我还有另一个看起来像这样的模块:

from bar import a_function
a_function()

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'
a_function()

import a_package.baz
a_package.baz.do_something_expensive = lambda: 'Something really cheap.'
a_function()

我希望得到结果:

Something expensive!
Something really cheap.
Something really cheap.

但是我得到了这个:

Something expensive!
Something expensive!
Something expensive!

我做错了什么?

【问题讨论】:

  • 第二个不起作用,因为您只是在本地范围内重新定义了 do_something_expensive 的含义。但是,我不知道为什么第三个不起作用...
  • 正如 Nicholas 解释的那样,您正在复制参考并仅替换其中一个参考。由于这个原因,from module import non_module_member 和模块级猴子补丁不兼容,通常最好避免使用。
  • 首选的包命名方案是小写,不带下划线,即apackage
  • @bobince,最好避免像这样的模块级可变状态,因为全局变量的不良后果早已被认识到。但是,from foo import bar 很好,实际上在适当的时候推荐。

标签: python monkeypatching


【解决方案1】:

想想 Python 命名空间的工作原理可能会有所帮助:它们本质上是字典。所以当你这样做时:

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'

这样想:

do_something_expensive = a_package.baz['do_something_expensive']
do_something_expensive = lambda: 'Something really cheap.'

希望你能明白为什么这不起作用:-) 一旦你将一个名字导入命名空间,你在 from 导入的命名空间中的名字的值就无关紧要了。您只是在上面的本地模块的命名空间或 a_package.baz 的命名空间中修改 do_something_expensive 的值。但是因为 bar 直接导入 do_something_expensive,而不是从模块命名空间中引用它,所以需要写入它的命名空间:

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'

【讨论】:

  • 第三方包呢,比如here
【解决方案2】:

有一个非常优雅的装饰器:Guido van Rossum: Python-Dev list: Monkeypatching Idioms

还有dectools 包,我看到了一个 PyCon 2010,它可能也可以在这种情况下使用,但实际上可能会相反(在方法声明级别的猴子补丁......你在哪里'不是)

【讨论】:

  • 那些装饰器似乎不适用于这种情况。
  • @MikeGraham:Guido 的电子邮件没有提到他的示例代码也允许替换任何方法,而不仅仅是添加一个新方法。所以,我认为它们确实适用于这个案例。
  • @MikeGraham Guido 示例确实非常适合模拟方法语句,我只是自己尝试过! setattr 只是一种花哨的说法 '=' ;所以'a = 3'要么创建一个名为'a'的新变量并将其设置为3,要么用3替换现有变量的值
【解决方案3】:

如果您只想为您的调用打补丁,否则保留原始代码,您可以使用https://docs.python.org/3/library/unittest.mock.html#patch(自 Python 3.3 起):

with patch('a_package.baz.do_something_expensive', new=lambda: 'Something really cheap.'):
    print do_something_expensive()
    # prints 'Something really cheap.'

print do_something_expensive()
# prints 'Something expensive!'

【讨论】:

    【解决方案4】:

    在第一个 sn-p 中,您使 bar.do_something_expensive 引用了 a_package.baz.do_something_expensive 在那个时刻引用的函数对象。要真正“monkeypatch”,您需要更改函数本身(您只是更改名称所指的内容);这是可能的,但您实际上并不想这样做。

    在您尝试改变a_function 的行为时,您做了两件事:

    1. 在第一次尝试中,您将 do_something_expensive 设置为模块中的全局名称。但是,您正在调用a_function,它不会查看您的模块来解析名称,因此它仍然引用相同的函数。

    2. 在第二个示例中,您更改了 a_package.baz.do_something_expensive 所指的内容,但 bar.do_something_expensive 并没有神奇地与之绑定。该名称仍然指的是它在初始化时查找的函数对象。

    最简单但远非理想的方法是将bar.py改为say

    import a_package.baz
    
    def a_function():
        print a_package.baz.do_something_expensive()
    

    正确的解决方案可能是以下两种情况之一:

    • 重新定义 a_function 以将函数作为参数并调用它,而不是试图潜入并更改硬编码引用的函数,或者
    • 将要使用的函数存储在类的实例中;这就是我们在 Python 中做可变状态的方式。

    使用全局变量(这是从其他模块更改模块级内容的原因)是一个坏事,它会导致难以维护、令人困惑、不可测试、不可扩展的代码,其流程难以跟踪。

    【讨论】:

      【解决方案5】:

      a_function() 函数中的do_something_expensive 只是模块命名空间中指向函数对象的变量。当您重新定义模块时,您是在不同的命名空间中进行的。

      【讨论】: