2

I have a costly function that I want to cache using functools.lru_cache, but only under certain runtime conditions.

decorator-based, no problem:

from functools import lru_cache


@lru_cache(maxsize=32)
def myfunc(a, b):
    return a * b


print(myfunc(1,2))
print(myfunc(1,2))
print(myfunc(1,3))

print(f"cache_info:{myfunc.cache_info()}")

output:

2
2
3
cache_info:CacheInfo(hits=1, misses=2, maxsize=32, currsize=2)

doesn't without @-based syntax

However, I run into problems when I try to declare the function first, then conditionally wrap it in lru_cache, without using @decorator syntax.

from functools import lru_cache

def conditional_cached_func(a, b):
    return a * b

some_system_dependent_value = True

if some_system_dependent_value:

    cache_done = False


    #  I'm trying multiple calls to see what lru_cache expects
    #    only #3 works, but just so you don't replicate the other ones.
    if not cache_done:

        try:
            conditional_cached_func = lru_cache(conditional_cached_func)
            cache_done = True
            print("#caching #1 worked")
        except (Exception,) as e: 
            print(f"caching #1 error: {e}")

    if not cache_done:

        try:
            conditional_cached_func = lru_cache(conditional_cached_func, maxsize=32)
            cache_done = True
            print("#caching #2 worked")
        except (Exception,) as e: 
            print(f"caching #2 error: {e}")


    if not cache_done:

        try:
            conditional_cached_func = lru_cache(32, conditional_cached_func)
            cache_done = True
            print("#caching #3 worked")
        except (Exception,) as e: 
            print(f"caching #3 error: {e}")

#but in fact #3 doesn't work when called.
print(conditional_cached_func(2,2))
print(conditional_cached_func(2,2))
print(conditional_cached_func(2,3))


if some_system_dependent_value:

    print(f"conditional_cache_info:{conditional_cached_func.cache_info()}")

output:


cache_info:CacheInfo(hits=1, misses=2, maxsize=32, currsize=2)
caching #1 error: Expected maxsize to be an integer or None
caching #2 error: lru_cache() got multiple values for argument 'maxsize'
#caching #3 worked
Traceback (most recent call last):
  File "test_147_lrucache.py", line 56, in <module>
    print(conditional_cached_func(2,2))
TypeError: decorating_function() takes 1 positional argument but 2 were given

I'm on Python 3.6, which I am mentioning as 3.8 apparently has tweaked some aspects of decorators that may be relevant.

Actually, it works under 3.8, with syntax #1

What do I need to do under 3.6?

#caching #1 worked
4
4
6
conditional_cache_info:CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)

If you're really, really, curious... this is hitting a mostly read-only database. While running tests, the result of my costly function to introspect the database can be assumed to be static and won't change. Under live conditions, those results can change at any time, but the users will pretty much never look at them.

Leopd
  • 41,333
  • 31
  • 129
  • 167
JL Peyret
  • 10,917
  • 2
  • 54
  • 73
  • 1
    See https://docs.python.org/3/library/functools.html#functools.lru_cache - `user_function`, i.e. the ability to use `@lru_cache` not `@lru_cache()`, was added in 3.8. Also you should read up on how decorators that accept arguments work - see e.g. https://stackoverflow.com/q/5929107/3001761, it would be `wrapped_func = decorator(decorator_arg)(wrapped_func)`. – jonrsharpe May 22 '20 at 21:04
  • So... `conditional_cached_func = lru_cache(maxsize=32)(conditional_cached_func)`? That seems to work in 3.6. And in 3.8. Do you want to put that as answer? sorry, decorators-with-arguments syntax is always when my brain starts to fizzle out in Python. – JL Peyret May 22 '20 at 21:10
  • 1
    @jonrsharp Why is this closed, considering I am not using @ syntax and am perfectly OK to specify a maxsize? It's not at all the same question. I.e. I want to use `lru_cache` without its syntactic sugar @. That's what my issue was about, not skipping maxsize. – JL Peyret May 22 '20 at 21:15
  • 2
    Because the answers there explain the same thing I did above - what changed in 3.8, and how to use it (i.e. `@lru_cache` vs. `@lru_cache()`) before that. The fact that you're applying it without the syntactic sugar doesn't really matter. – jonrsharpe May 22 '20 at 21:16
  • 3
    @jonrsharpe that's not how duplicate questions work. Just because an answer to a different question contains information about solving this problem, that is not sufficient to make it a duplicate question. Even worse if you need to combine two questions' answers to figure it out! This should clearly be re-opened. – Leopd May 14 '22 at 01:11

1 Answers1

5

A new syntax @functools.lru_cache(user_function) has been added in 3.8, that probably explains the difference in behaviour.

As for lru_cache(32, conditional_cached_func), it does not actually work because the second argument is passed to optional boolean parameter typed, and not the function to cache. See lru_cache documentation for details on its parameters.

conditional_cached_func = functools.lru_cache(maxsize=32)(user_function) (e.g. creating a decorator then applying it to a function) will work for both versions, but it is not very Pythonic...

Didier
  • 430
  • 6
  • 15