Bind/normalize params for @functools.cache

I was surprised to find, when I pass arguments to a function decorated with `@functools.cache` in different, equivalent ways, the cache does not recognize them as the same. counter = itertools.count(1) @functools.cache def example(a, b, c=0): return (next(counter), a, b, c) example(1, 2) # => (1, 1, 2, 0) example(1, b=2) # => (2, 1, 2, 0) example(1, 2, 0) # => (3, 1, 2, 0) When I wrote my own implementation as a coding exercise, I noticed the same weakness while testing it and solved that by having the decorator function get the signature of the decorated function, then use the bind method of the signature to bind the parameter values, then call the apply_defaults method on the bound arguments, and then finally, use the args and kwargs properties of the bound arguments to make the cache key. It seems like functools.cache should do the same thing. If it is undesirable for that to be the default behavior, then it could be optional (e.g. @functools.cache(normalize=True) ). I have not tested to see if functools.lru_cache has the same issue. I presume that it does, so my suggestion would apply to that as well.

Steve Jorgensen wrote:
After saying that, I realized that, if the behavior should be optional, then maybe it would make sense to provide another wrapper to normalize the parameters instead (see possible implementation below)? On the other hand, since the primary use of such a thing would be for caching, maybe it does make more sense to include the behavior in 'functools.cache' et al., as I originally suggested, or maybe have both. def bind_call_params(func): """ Transform a function to always receive its arguments in the same form (which are positional and which are keyword) even if its implementation is less strict than what is described by its signature. This is for use in cases where the form of in which the parameters are passed may be significant to a decorator (e.g. '@functools.cache'). """ sig = signature(func) @wraps(func) def wrapper(*args, **kwargs): bound = sig.bind(*args, **kwargs) bound.apply_defaults() return func(*bound.args, **bound.kwargs) return wrapper

Steve Jorgensen wrote:
After saying that, I realized that, if the behavior should be optional, then maybe it would make sense to provide another wrapper to normalize the parameters instead (see possible implementation below)? On the other hand, since the primary use of such a thing would be for caching, maybe it does make more sense to include the behavior in 'functools.cache' et al., as I originally suggested, or maybe have both. def bind_call_params(func): """ Transform a function to always receive its arguments in the same form (which are positional and which are keyword) even if its implementation is less strict than what is described by its signature. This is for use in cases where the form of in which the parameters are passed may be significant to a decorator (e.g. '@functools.cache'). """ sig = signature(func) @wraps(func) def wrapper(*args, **kwargs): bound = sig.bind(*args, **kwargs) bound.apply_defaults() return func(*bound.args, **bound.kwargs) return wrapper
participants (1)
-
Steve Jorgensen