计算在装饰器

时间:2017-04-13 00:15:28

标签: python python-3.x python-decorators

我有这堂课:

class SomeClass(object):
    def __init__(self):
        self.cache = {}
    def check_cache(method):
        def wrapper(self):
            if method.__name__ in self.cache:
                print('Got it from the cache!')
                return self.cache[method.__name__]
            print('Got it from the api!')
            self.cache[method.__name__] = method(self)
            return self.cache[method.__name__]
        return wrapper
    @check_cache
    def expensive_operation(self):
        return get_data_from_api()

def get_data_from_api():
    "This would call the api."
    return 'lots of data'

我的想法是,如果结果已经缓存,我可以使用@check_cache装饰器使expensive_operation方法不再调用api。

这看起来很好。

>>> sc.expensive_operation()
Got it from the api!
'lots of data'
>>> sc.expensive_operation()
Got it from the cache!
'lots of data'

但我希望能够用另一位装饰师测试它:

import unittest

class SomeClassTester(SomeClass):
    def counted(f):
        def wrapped(self, *args, **kwargs):
            wrapped.calls += 1
            return f(self, *args, **kwargs)
        wrapped.calls = 0
        return wrapped
    @counted
    def expensive_operation(self):
        return super().expensive_operation()


class TestSomeClass(unittest.TestCase):
    def test_api_is_only_called_once(self):
        sc = SomeClassTester()
        sc.expensive_operation()
        self.assertEqual(sc.expensive_operation.calls, 1) # is 1
        sc.expensive_operation()
        self.assertEqual(sc.expensive_operation.calls, 1) # but this goes to 2

unittest.main()

问题是counted装饰器计算调用wrapper函数的次数,而不是此内部函数。

如何从SomeClassTester计算出来?

2 个答案:

答案 0 :(得分:2)

没有简单的方法可以做到这一点。您当前的测试以错误的顺序应用装饰器。您想要check_cache(counted(expensive_operation)),但是您在外面获得了counted装饰器:counted(check_cache(expensive_operation))

counted装饰器中没有简单的方法可以解决这个问题,因为当它被调用时,原始函数已经被check_cache装饰器包裹起来,并且没有简单的方法可以改变包装器(它在闭包单元中保存对原始函数的引用,从外部只读它。)

使其工作的一种可能方法是使用所需顺序的装饰器重建整个方法。您可以从闭包单元格中获取对原始方法的引用:

class SomeClassTester(SomeClass):
    def counted(f):
        def wrapped(self, *args, **kwargs):
            wrapped.calls += 1
            return f(self, *args, **kwargs)
        wrapped.calls = 0
        return wrapped
    expensive_operation = SomeClass.check_cache(
        counted(SomeClass.expensive_operation.__closure__[0].cell_value)
    )

这当然远非理想,因为您需要确切知道在SomeClass中对方法应用了哪些装饰器才能再次正确应用它们。您还需要知道这些装饰器的内部,以便您可以获得正确的闭包单元格(如果另一个装饰器被更改为不同,则[0]索引可能不正确。)

另一种(也许是更好的)方法可能是以这样的方式更改SomeClass,您可以在更改的方法和要计算的昂贵位之间注入计数代码。例如,您可以将真正昂贵的部分放在_expensive_method_implementation中,而装饰的expensive_method只是一个调用它的简单包装器。测试类可以使用自己的装饰版本覆盖_implementation方法(甚至可以跳过实际昂贵的部分,只返回虚拟数据)。它不需要覆盖常规方法或乱七八糟的装饰器。

答案 1 :(得分:1)

如果不修改基类来提供钩子或根据基类的内部知识更改派生类中的整个修饰函数,则不可能这样做。虽然有第三种方法基于缓存装饰器的内部工作,但基本上改变你的缓存dict以便计算

class CounterDict(dict):
  def __init__(self, *args):
    super().__init__(*args)
    self.count = {}

  def __setitem__(self, key, value):
    try:
      self.count[key] += 1
    except KeyError:
      self.count[key] = 1
    return super().__setitem__(key, value)


class SomeClassTester(SomeClass):
    def __init__(self):
      self.cache = CounterDict()

class TestSomeClass(unittest.TestCase):
    def test_api_is_only_called_once(self):
        sc = SomeClassTester()
        sc.expensive_operation()
        self.assertEqual(sc.cache.count['expensive_operation'], 1) # is 1
        sc.expensive_operation()
        self.assertEqual(sc.cache.count['expensive_operation'], 1) # is 1