Python 模拟方法调用参数显示列表的最后状态

11 投票
4 回答
4704 浏览
提问于 2025-04-18 04:00

我有一个函数,它需要一个列表作为参数。这个函数会被多次调用,每次调用时,列表中的一些值会被更新。我用来捕捉调用参数的模拟对象,总是显示列表中最新的值,而不是每次调用时的值。下面的代码展示了这个问题。

from mock import MagicMock

def multiple_calls_test():
    m = MagicMock()
    params = [0, 'some_fixed_value', 'some_fixed_value']
    for i in xrange(1,10):
        params[0] = i
        m(params)
    for args in m.call_args_list:
        print args[0][0]

multiple_calls_test()

这是输出结果,注意所有的调用中,第一个列表元素都是9。

[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']
[9, 'some_fixed_value', 'some_fixed_value']

有没有办法让这个模拟对象复制列表参数,而不是直接引用实际的列表?或者有没有其他方法可以确保每次方法执行时的值都是正确的?谢谢。

4 个回答

-2

这是因为调用参数列表的懒惰求值。

当你写 funcs = [lambda x: x + a for a in [2, 3]] 时,它并不会定义两个不同的函数,一个是加2,另一个是加3。

实际上,它定义了两个函数,都是加3。

而且,copy() 可能会在调用之前就发生。

0

试试 m.mock_calls。这个可以列出所有被调用的记录。我觉得这样应该可以:

>>> from unittest.mock import MagicMock, call
>>> m = MagicMock()
>>> m('abc')
<MagicMock name='mock()' id='2634881401576'>
>>> m('def')
<MagicMock name='mock()' id='2634881401576'>
>>> call('abc') in m.mock_calls
True
>>> call('ghi') in m.mock_calls
False
1

对于Python 3.8,之前的解决方案对我来说不再有效。
不过,官方的Python文档里有一个解决办法:
https://docs.python.org/3/library/unittest.mock-examples.html#coping-with-mutable-arguments
你需要往下滚动一点才能找到以下内容:

另一种方法是创建一个Mock或MagicMock的子类,这个子类会使用copy.deepcopy()来复制参数。下面是一个示例实现:

from copy import deepcopy
class CopyingMock(MagicMock):
    def __call__(self, /, *args, **kwargs):
        args = deepcopy(args)
        kwargs = deepcopy(kwargs)
        return super(CopyingMock, self).__call__(*args, **kwargs)

这个方法在我使用Python 3.8时有效。

11

不幸的是,这看起来是mock库的一个缺陷。从代码来看,要实现这个功能似乎需要对mock库进行修改。不过,有一种比较简单的方法可以达到你想要的效果:

import copy
from mock import MagicMock


class CopyArgsMagicMock(MagicMock):
    """
    Overrides MagicMock so that we store copies of arguments passed into calls to the
    mock object, instead of storing references to the original argument objects.
    """

    def _mock_call(_mock_self, *args, **kwargs):
        args_copy = copy.deepcopy(args)
        kwargs_copy = copy.deepcopy(kwargs)
        return super(CopyArgsMagicMock, self)._mock_call(*args_copy, **kwargs_copy)

然后(说得很明显)只需把你的MagicMock替换成CopyArgsMagicMock,你应该就能看到想要的效果。

请注意,这个方法只在提供的使用案例中进行了测试,所以可能并不是一个完整和稳健的解决方案,但希望它能对你有所帮助。

撰写回答