Python 模拟方法调用参数显示列表的最后状态
我有一个函数,它需要一个列表作为参数。这个函数会被多次调用,每次调用时,列表中的一些值会被更新。我用来捕捉调用参数的模拟对象,总是显示列表中最新的值,而不是每次调用时的值。下面的代码展示了这个问题。
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 个回答
这是因为调用参数列表的懒惰求值。
当你写 funcs = [lambda x: x + a for a in [2, 3]] 时,它并不会定义两个不同的函数,一个是加2,另一个是加3。
实际上,它定义了两个函数,都是加3。
而且,copy() 可能会在调用之前就发生。
试试 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
对于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时有效。
不幸的是,这看起来是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
,你应该就能看到想要的效果。
请注意,这个方法只在提供的使用案例中进行了测试,所以可能并不是一个完整和稳健的解决方案,但希望它能对你有所帮助。