【问题标题】:Count calls of a method that may or may not be called inside a decorator计算在装饰器中可能会或可能不会被调用的方法的调用次数
【发布时间】:2017-09-08 22:41:52
【问题描述】:

我有这门课:

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 计算?

【问题讨论】:

  • 这对我有用 (AssertionError: 2 != 1) 吗?或者你想那样,你想数一下,你已经装饰了两次的原始昂贵操作?
  • 我希望第二个断言为 1,因为该方法不会被第二次调用,而是会返回缓存调用的值。

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


【解决方案1】:

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

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

【讨论】:

  • 这是完美的,因为我不需要修改我的基类。很有创意!
【解决方案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 方法(甚至可能跳过实际昂贵的部分,只返回虚拟数据)。它不需要重写常规方法或弄乱它的装饰器。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-05
    • 1970-01-01
    • 2021-09-10
    • 2011-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多