A cache decorator that preserves the original function signature?

My class has a few regex methods that I would like to cache. I immediately reached my hand out to functools.cache but I found out that it was destroying my original function signature which is a deal breaker for me. Is there something similar that also preserves the original function signature?

You can add it yourself:

new_func = functools.cache(func)
new_func.__signature__ = inspect.signature(func)
1 Like

Can I use this as a decorator?

You can make your own decorator which wraps these 2 lines inside of it.

Let me know if you can not work out how, I will give you a hand.

1 Like

Here’s what i’ve managed to come up with

from functools import lru_cache
from time import perf_counter, sleep
from typing import Callable, ParamSpec, TypeVar

T = TypeVar("T")
P = ParamSpec("P")


def cache(func: Callable[P, T], /) -> Callable[P, T]:
    return lru_cache(maxsize=None)(func)  # Incompatible return value type (got "_lru_cache_wrapper[T]", expected "Callable[P, T]") Mypy return-value


@cache
def expensive_function(a: int = 0, b: int | str | None = None) -> str:
    sleep(10)
    return "hello"

start = perf_counter()
result = expensive_function(20, None)
end = perf_counter()
print(end - start)

start = perf_counter()
result = expensive_function(20, None)
end = perf_counter()
print(end - start)

start = perf_counter()
result = expensive_function(20, None)
end = perf_counter()
print(end - start)

I’ve never typed a decorator before so I’m still unsure if what I’ve done is correct but it does manage to preserve the signature. (Thanks to this arcticle here.)
image

Although I’m unsure if my decorator is even wokring:

10.000129600000946
2.2999993234407157e-06
6.999998731771484e-07

How come the third call takes so much longer?

To me it seems to be A LOT faster. It is 7 / 10**7.

I don’t use typing so will not be of much help with that approach. Neither do I use mypy. So maybe someone else can help you with this.

I would simply do it:

def cache(func):
    wrapper = functools.cache(func)
    wrapper.__signature__ = inspect.signature(func)
    return wrapper
1 Like

There has been a lot of discussions and multiple attempts at making lru_cache preserve the typing signature, but this is hard because it also has to work with methods. See Use ParamSpec for lru_cache by cdce8p · Pull Request #11662 · python/typeshed · GitHub and related issues/PR.

For your more limited usecase, you can use a cast or # type: ignore to get the correct function signature.

2 Likes

Okay. Thanks alot everyone!