为什么 unittest 的 mock.patch.start' 会重新运行启动补丁程序的函数?

Why does unittest's `mock.patch.start` re-run the function in which the patcher is started?

提问人:AmagicalFishy 提问时间:10/27/2023 最后编辑:AmagicalFishy 更新时间:11/10/2023 访问量:116

问:

假设我们有两个文件:

to_patch.py

from unittest.mock import patch

def patch_a_function():
    print("Patching!")
    patcher = patch("to_be_patched.function")
    patcher.start()
    print("Done patching!")

to_be_patched.py

from to_patch import patch_a_function

def function():
    pass

patch_a_function()
function()

我们运行.这将输出:python -m to_be_patched

Patching!
Patching!
  1. 为什么从来没有打印过?Done patching!
  2. 为什么打印两次?Patching!

我已将答案缩小到(2)向下;调用似乎再次触发。我怀疑这是因为它被导入了,但不确定为什么函数本身会第二次运行。同样,我不确定为什么在对 的任何一个调用中都没有到达该行。 不能阻塞,因为程序退出得很好,而不是挂在那里......右?patch.startpatch_a_functionto_be_patched.pyDone patching!patch_a_functionpatcher.start()

编辑:哼。看起来没有人可以复制被打印(老实说,这是主要困难)——所以我想这只是我这边的问题Done patching!

python-unittest 补丁 monkeypatching python-unittest.mock

评论

1赞 Oluwafemi Sule 11/5/2023
第一个“修补”是通过在模块中调用patch_a_function来打印的。第二个 “Patching” 是通过在模块中调用 'patch(“to_be_patched.function”)' 来打印的。另外,我在python上打印了“完成修补!你在哪个版本上运行它?to_be_patchedto_patch3.10.0

答:

0赞 puchal 11/6/2023 #1

这里出现的问题与 无关,而是由于循环导入和从脚本的顶层调用造成的。patcher.start()patch_a_function

当您运行时,它会调用 .to_be_patchedpatch_a_function

现在需要导入,当它被调用时,它会再次调用。patch_a_functionpatcher = patch("to_be_patched.function")to_be_patchedpatch_a_function

这次调用时,它不会重新导入,因为它已经导入,因此从这一点开始,脚本会按预期继续。patch("to_be_patched.function")to_be_patched

您可以将步骤描述为:

 1. CALL patch_a_function
 2. print("Patching!")
 3. CALL `patch("to_be_patched.function")`
 4. `patch` CALLS __import__ of `to_be_patched`
 5. `to_be_patched` is not imported yet so import module
 6. During the import CALL patch_a_function
 7. print("Patching!")
 8. CALL `patch("to_be_patched.function")`
 9. `patch` CALLS __import__ of `to_be_patched`
 10. `to_be_patched` is already imported
 11. Finish the rest of second time called 
 12. CALL patcher.start()
 13. print("Done patching!")
 14. CALL function() (still it is done since we import the module) 
 15. CALL patcher.start()
 16. print("Done patching!")
 17. CALL function()

如果要解决在语句中调用函数的问题:to_be_patchedif __name__ == '__main__'

from to_patch import patch_a_function

def function():
    pass

if __name__ == '__main__':
    patch_a_function()
    function()

下面是有关顶级代码环境的详细信息:https://docs.python.org/3/library/__main__.html

评论

0赞 AmagicalFishy 11/7/2023
既然我们陷入了导入循环,为什么它不能无限打印呢?Patching!
0赞 puchal 11/7/2023
@AmagicalFishy对不起,我的错。使用这个词并不能完美地描述这里的情况。 将导入一次,导入过程中会再次调用。然后将再次导入,但由于它已经导入,它将返回之前导入的模块(在多次导入同一模块期间会发生什么:stackoverflow.com/questions/37067414/...)我将编辑答案并删除误导性looppatchto_be_patchedpatch_a_functionpatchto_be_patchedloop
0赞 puchal 11/7/2023
我无法重现未打印的情况。Done patching!
2赞 wim 11/7/2023 #2
  1. 为什么从来没有打印过?Done patching!

无法复制。

$ python -m to_be_patched
Patching!
Patching!
Done patching!
Done patching!
  1. 为什么打印两次?Patching!

您的模块被导入两次。如果添加到文件中,则会很清楚:print(__name__)to_be_patched.py

from to_patch import patch_a_function

print(f"{__name__=}")

def function():
    pass

patch_a_function()
function()  # note: this line doesn't actually do anything, and could be commented out

结果:

$ python -m to_be_patched
__name__='__main__'
Patching!
__name__='to_be_patched'
Patching!
Done patching!
Done patching!

当您使用时,您的模块将被加载为顶级代码,即该模块将是 .python -m to_be_patchedto_be_patched__name__"__main__"

使用时,mock 将首先导入补丁目标。当将补丁目标作为字符串时,如 mock 将使用 ,via pkgutil.resolve_name,以查找要修补的正确命名空间。此方法使用 as 加载目标模块,它不是顶级代码环境。尽管加载的是相同的底层 .py 文件,但由于名称不匹配,缓存未命中: 。mock.patch"to_be_patched.function"importlib__name__"to_be_patched"sys.modules"__main__" != "to_be_patched"

该函数现在具有双重身份,并且存在于模块和模块中,因此您看到的是每个被调用的函数。第一个调用触发第二个调用,通过所述的双重导入机制。patch_a_function__main__to_be_patched