[英]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.