簡體   English   中英

計算在裝飾器內調用或不調用的方法的調用

[英]Count calls of a method that may or may not be called inside a decorator

我有這門課:

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算出來?

沒有簡單的方法可以做到這一點。 您當前的測試以錯誤的順序應用裝飾器。 你想要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方法(甚至可以跳過實際昂貴的部分並返回虛擬數據)。 它不需要覆蓋常規方法或其裝飾器亂七八糟。

如果不修改基類來提供鈎子或根據基類的內部知識更改派生類中的整個修飾函數,則不可能這樣做。 雖然有第三種方法基於緩存裝飾器的內部工作,但基本上改變你的緩存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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM