如何测试记忆函数?
Posted
技术标签:
【中文标题】如何测试记忆函数?【英文标题】:How can memoized functions be tested? 【发布时间】:2015-07-09 07:56:44 【问题描述】:我有一个简单的记忆器,我用它来节省昂贵的网络调用时间。大致来说,我的代码如下所示:
# mem.py
import functools
import time
def memoize(fn):
"""
Decorate a function so that it results are cached in memory.
>>> import random
>>> random.seed(0)
>>> f = lambda x: random.randint(0, 10)
>>> [f(1) for _ in range(10)]
[9, 8, 4, 2, 5, 4, 8, 3, 5, 6]
>>> [f(2) for _ in range(10)]
[9, 5, 3, 8, 6, 2, 10, 10, 8, 9]
>>> g = memoize(f)
>>> [g(1) for _ in range(10)]
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
>>> [g(2) for _ in range(10)]
[8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
"""
cache =
@functools.wraps(fn)
def wrapped(*args, **kwargs):
key = args, tuple(sorted(kwargs))
try:
return cache[key]
except KeyError:
cache[key] = fn(*args, **kwargs)
return cache[key]
return wrapped
def network_call(user_id):
time.sleep(1)
return 1
@memoize
def search(user_id):
response = network_call(user_id)
# do stuff to response
return response
我对此代码进行了测试,我在其中模拟了 network_call()
的不同返回值,以确保我在 search()
中所做的一些修改按预期工作。
import mock
import mem
@mock.patch('mem.network_call')
def test_search(mock_network_call):
mock_network_call.return_value = 2
assert mem.search(1) == 2
@mock.patch('mem.network_call')
def test_search_2(mock_network_call):
mock_network_call.return_value = 3
assert mem.search(1) == 3
但是,当我运行这些测试时,我会失败,因为 search()
返回一个缓存的结果。
CAESAR-BAUTISTA:~ caesarbautista$ py.test test_mem.py
============================= test session starts ==============================
platform darwin -- Python 2.7.8 -- py-1.4.26 -- pytest-2.6.4
collected 2 items
test_mem.py .F
=================================== FAILURES ===================================
________________________________ test_search_2 _________________________________
args = (<MagicMock name='network_call' id='4438999312'>,), keywargs =
extra_args = [<MagicMock name='network_call' id='4438999312'>]
entered_patchers = [<mock._patch object at 0x108913dd0>]
exc_info = (<class '_pytest.assertion.reinterpret.AssertionError'>, AssertionError(u'assert 2 == 3\n + where 2 = <function search at 0x10893f848>(1)\n + where <function search at 0x10893f848> = mem.search',), <traceback object at 0x1089502d8>)
patching = <mock._patch object at 0x108913dd0>
arg = <MagicMock name='network_call' id='4438999312'>
@wraps(func)
def patched(*args, **keywargs):
# don't use a with here (backwards compatability with Python 2.4)
extra_args = []
entered_patchers = []
# can't use try...except...finally because of Python 2.4
# compatibility
exc_info = tuple()
try:
try:
for patching in patched.patchings:
arg = patching.__enter__()
entered_patchers.append(patching)
if patching.attribute_name is not None:
keywargs.update(arg)
elif patching.new is DEFAULT:
extra_args.append(arg)
args += tuple(extra_args)
> return func(*args, **keywargs)
/opt/boxen/homebrew/lib/python2.7/site-packages/mock.py:1201:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
mock_network_call = <MagicMock name='network_call' id='4438999312'>
@mock.patch('mem.network_call')
def test_search_2(mock_network_call):
mock_network_call.return_value = 3
> assert mem.search(1) == 3
E assert 2 == 3
E + where 2 = <function search at 0x10893f848>(1)
E + where <function search at 0x10893f848> = mem.search
test_mem.py:15: AssertionError
====================== 1 failed, 1 passed in 0.03 seconds ======================
有没有办法测试记忆函数?我考虑了一些替代方案,但它们都有缺点。
一种解决方案是模拟memoize()
。我不愿意这样做,因为它会将实现细节泄露给测试。从理论上讲,我应该能够在没有系统其余部分(包括测试)的情况下从功能的角度来记忆和取消记忆功能。
另一种解决方案是重写代码以公开装饰函数。也就是说,我可以这样做:
def _search(user_id):
return network_call(user_id)
search = memoize(_search)
然而,这会遇到与上述相同的问题,尽管它可能会更糟,因为它不适用于递归函数。
【问题讨论】:
不确定我是否理解。为什么你有两个测试来测试不同的返回值?如果您的记忆函数可以返回“陈旧”值(与网络中的实时值不同),那么您不应该测试两个值。如果不行,那么您需要使您的记忆更加复杂,以便您可以在必要时以某种方式使缓存无效。如果您无法知道何时可以使用记忆值与获取实际值,那么记忆是没有用的。 好的,想象一下下面的真实世界场景。您使用输入1
调用 memoized search
函数,它返回 1(因为这是服务器当前的行为方式)。假设将来服务器状态发生变化,因此当您传入1
时它会返回其他内容。记忆化的search
函数的预期行为是否总是返回1
?
@CeasarBautista:如果您的代码的实际用户在 network_call
返回 2 的条件下调用 mem.search(1)
,然后在 network_call
返回 3 的条件下调用 mem.search(1)
,应该怎么做结果是?如果您希望调用是独立的,则需要使缓存无效。如果您从不使缓存失效,network_call
行为的更改将永远不会影响以后对mem.search
的调用(使用相同的参数)。
@CeasarBautista 这仅适用于不影响环境状态的单元测试。您的单元测试并非如此(包装函数的缓存已发生突变)。如果您希望您的单元测试被隔离,您需要在它们之间运行拆卸和设置。在这种情况下,这将涉及根据其定义和装饰器重新创建搜索函数,或者以其他方式清除记忆缓存。
@CeasarBautista:他们无法隔离全局状态,这实际上就是你的记忆。如果一个测试设置了一个全局变量blah = 1
,其他测试也会看到。同样,memoized 值是全局存储的(在 memoized 函数的闭包中)。如果您希望记忆发生在某个非全局级别,您需要将search
设为非全局函数(例如,某个类的方法)。或者,同样,您可以提供某种使缓存无效的“手动覆盖”。
【参考方案1】:
您应该分别测试每个问题:
你已经展示了memoize
,我假设你已经测试过了。
你似乎有network_call
,所以你应该单独测试它,而不是记忆。
现在您想将两者结合起来,但大概这将有利于其他代码避免冗长的网络延迟。但是,如果您想测试其他代码,那么它甚至不应该进行 1 次网络调用,因此您可能必须提供函数名称作为参数。
【讨论】:
【参考方案2】:在函数级别定义你的记忆真的很可取吗?
这有效地使记忆数据成为一个全局变量(就像函数一样,它共享其范围)。
顺便说一句,这就是你在测试它时遇到困难的原因!
那么,如何将它包装到一个对象中呢?
import functools
import time
def memoize(meth):
@functools.wraps(meth)
def wrapped(self, *args, **kwargs):
# Prepare and get reference to cache
attr = "_memo_0".format(meth.__name__)
if not hasattr(self, attr):
setattr(self, attr, )
cache = getattr(self, attr)
# Actual caching
key = args, tuple(sorted(kwargs))
try:
return cache[key]
except KeyError:
cache[key] = meth(self, *args, **kwargs)
return cache[key]
return wrapped
def network_call(user_id):
print "Was called with: %s" % user_id
return 1
class NetworkEngine(object):
@memoize
def search(self, user_id):
return network_call(user_id)
if __name__ == "__main__":
e = NetworkEngine()
for v in [1,1,2]:
e.search(v)
NetworkEngine().search(1)
产量:
Was called with: 1
Was called with: 2
Was called with: 1
换句话说,NetworkEngine
的每个实例都有自己的缓存。只需重复使用同一个缓存即可共享缓存,或实例化一个新缓存以获得新缓存。
在您的测试代码中,您会使用:
@mock.patch('mem.network_call')
def test_search(mock_network_call):
mock_network_call.return_value = 2
assert mem.NetworkEngine().search(1) == 2
【讨论】:
+1 是一个很好的答案,但如果您使用 OP 的原始代码,可能会更快理解这一点。以上是关于如何测试记忆函数?的主要内容,如果未能解决你的问题,请参考以下文章