Maurice
Maurice

Reputation: 13107

Pythons lru_cache on inner function doesn't seem to work

I'm trying to use functools.lru_cache to cache the result of an inner function, but the cache doesn't seem to work as expected.

I have a function that performs some logic and then calls a rather expensive function. I'd like to cache the result of the expensive function call and though I'd just apply the lru_cache to an inner function. Unfortunately the behavior isn't as expected - the expensive function gets called every time even though the arguments to the inner function are identical.

I've created a (simplified) test case to show the behavior:

import unittest
from functools import lru_cache
from unittest.mock import patch

def expensive_function(parameter: str) -> str:
    return parameter

def partially_cached(some_parameter: str) -> str:

    @lru_cache
    def inner_function(parameter: str):
        return expensive_function(parameter)

    result = inner_function(some_parameter)

    print(inner_function.cache_info())

    return result

class CacheTestCase(unittest.TestCase):

    def test_partially_cached(self):
        
        with patch(self.__module__ + ".expensive_function") as expensive_mock:

            expensive_mock.return_value = "a"

            self.assertEqual(partially_cached("a"), "a")
            self.assertEqual(partially_cached("a"), "a")

            # If the cache works, I expect the expensive function
            # to be called just once for the same parameter
            expensive_mock.assert_called_once()

if __name__ == "__main__":
    unittest.main()

(In this case I wouldn't need the inner function, but as I said - it's simplified)

Unfortunately the test fails

python3 /scratch/test_cache.py
CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)
CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)
F
======================================================================
FAIL: test_partially_cached (__main__.CacheTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
[...]
AssertionError: Expected 'expensive_function' to have been called once. Called 2 times.
Calls: [call('a'), call('a')].

----------------------------------------------------------------------
Ran 1 test in 0.004s

FAILED (failures=1)

I probably have a misunderstanding concerning either inner functions or the lru_cache, but I'm not sure which it is - I appreciate all the help I can get.

Upvotes: 12

Views: 3627

Answers (1)

progmatico
progmatico

Reputation: 4964

It does not work as intended because the inner_function gets redefined each time partially_cached is called, and so is the cached version. So each cached version gets called only once.

See memoizing-decorator-keeping-stored-values

Additionally, if you mock a decorated function you need to apply the decorator again.

See how-to-mock-a-decorated-function

If you decorate at the outer level, or decorate another extracted to outer level function, it will work, if you don't break it with mocking.

Upvotes: 9

Related Questions