Use functools' @lru_cache without specifying maxsize parameter
Asked Answered
O

3

38

The documentation for lru_cache gives the function definition:

@functools.lru_cache(maxsize=128, typed=False)

This says to me that maxsize is optional.

However, it doesn't like being called without an argument:

Python 3.6.3 (default, Oct 24 2017, 14:48:20) 
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import functools
>>> @functools.lru_cache
... def f(): ...
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/functools.py", line 477, in lru_cache
    raise TypeError('Expected maxsize to be an integer or None')
TypeError: Expected maxsize to be an integer or None
 >>> 

Calling with an argument is fine:

>>> @functools.lru_cache(8)
... def f(): ...
... 
>>> 

Am I misreading the documentation?

Osmunda answered 10/11, 2017 at 8:14 Comment(1)
Using @lru_cache without parentheses works starting with Python 3.8.Onstage
B
34

You have to at least call lru_cache without args:

@lru_cache()
def f():
    #content of the function

This way, lru_cache is initialized with default parameters.

This is because decorators in python (with the @ notation) are special functions which are evaluated and called when the interpreter is importing the module.

When you write @decorator_name you tell python that decorator_name is a function that will be called with the function (or class) defined after. Example:

@my_decorator
def function():
    pass

is equivalent to:

def function():
    pass
decorated_function = my_decorator(function)

The lru_cache decorator is a little bit more complex because before wrapping the function, it has to create the cache (related to the function), and then wrap the function with another function that will do the cache management. Here is the (shorted) code of the CPython implementation :

def lru_cache(maxsize=128, typed=False):
    # first, there is a test about the type of the parameters
    if maxsize is not None and not isinstance(maxsize, int):
        raise TypeError('Expected maxsize to be an integer or None')
    # then, the decorating function is created, this function will be called each time you'll call the 'cached' function
    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)  # in _lru_wrapper is all the magic about the cache management, it is a 2nd layer of decorator
        return update_wrapper(wrapper, user_function)
    return decorating_function

So, when you wrote only

@lru_cache
def f():

python called lru_cache(f), and definitively, it wasn't made to handle such thing.

To make it compliant with this write, we should add a test to check if the first parameter (maxsize) is a callable function:

def lru_cache(maxsize=128, typed=False):
    # first, there is a test about the type of the parameters
    if callable(maxsize):
        def decorating_function(user_function):
            wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
            return update_wrapper(wrapper, user_function)
        return decorating_function(maxsize) # yes, maxsizeis the function in this case O:)
    if maxsize is not None and not isinstance(maxsize, int):
        raise TypeError('Expected maxsize to be an integer or None')
    # then, the decorating function is created, this function will be called each time you'll call the 'cached' function
    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)  # in _lru_wrapper is all the magic about the cache management, it is a 2nd layer of decorator
        return update_wrapper(wrapper, user_function)
    return decorating_function
Baalbek answered 10/11, 2017 at 8:18 Comment(6)
Thanks! Where does the token user_function defined in your code?Osmunda
@TomHale user_fuinction is the name of the parameter of the internal decorator, it is the/your decorated functionIslek
Cheers! I raised this issue based on your answer.Osmunda
@TomHale nice, BTW, it isn't a real code issue, probably more a hole in the documentation.Islek
To me, Least Surprise says that most decorators don't require () after them... I really like what you've done with not requiring them.Osmunda
Using @lru_cache without parentheses works starting with Python 3.8.Onstage
O
26

Starting with Python 3.8+ you can use @lru_cache without parentheses, so your code snippet will work as-is

Python 3.8.0 (default, Oct 28 2019, 16:14:01) 
[GCC 9.2.1 20191008] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import functools
>>> @functools.lru_cache
... def f():
...     return 2
... 
>>> 

On Python 3.7 or older, you have to do @lru_cache(). As in, add parentheses after @lru_cache

P.S. @lru_cache with no arguments implicitly sets max_size to 128. If you want to use a cache with no max size instead, on Python 3.9+ you can use the functools.cache decorator, which acts like lru_cache(max_size=None).

Onstage answered 6/12, 2019 at 16:55 Comment(0)
W
3

Think about it that way: lru_cache is a decorator factory. You call it (with or without params, but you call it) and it gives you a decorator.

Calling the decorator factory and applying the decorator on one line is the equivalent of this:

with_small_cache = lru_cache(max_size=5)

@with_small_cache
def function():
    ...
Water answered 10/11, 2017 at 17:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.