There is one pitfall that may be relevant to your implementation. Observe
def pf(*args, **kwargs):
print(args)
print(kwargs)
and call this with
pf(1, k="a")
pf(1, "a")
pf(k="a", x=1)
All argument specs are valid specs for a function with signature f(x, k) (with or without defaults) - so you can't really know the order of the arguments, their names, and sorting on kwargs is definitely not enough in a general case (empty in the second example, while args is empty in the last with order reversed). Defaults make this worse as if f(x, k=3) is the definition, then f(2, 3) and f(2) and f(x=2) f(2, k=3) and f(x=2, k=3) (also reversed) are the same, with differing kwargs and args passed to the wrapper.
A more robust solution will use inspect.getargspec(your_function). This uses reflection to know the actual argument names of the function as they were defined. You then have to "fill in" the arguments your are given in *args and **kwargs, and use that to generate your call signature:
import inspect
def f(x, k=3): pass
argspec = inspect.getargspec(f) # returns ArgSpec(args=['x', 'k'], varargs=None, keywords=None, defaults=(3,))
Now you can generate a call signature (from *args and **kwargs):
signature = {}
for arg, default in zip(reversed(argspec.args), reversed(argspec.defaults)):
signature[arg] = default
set_args = set()
for arg, val in zip(argspec.args, args):
set_args.add(arg)
signature[arg] = val
for arg, val in kwargs.items():
# if arg in set_args:
# raise TypeError(f'{arg} set both in kwargs and in args!')
# if arg not in argspec.args:
# raise TypeError(f'{arg} is not a valid argument for function!')
signature[arg] = val
# if len(signature) == len(argspec.args):
# raise TypeError(f'Received {len(signature)} arguments but expected {len(argspec.args)} arguments!')
Then you can use the dictionary signature itself as the call signature. I showed some "correctness" checks above though you may just want to let the call itself to the function detect and fail. I did not handle functions with **kwargs and *args (the actual used names are given in argspec). I think they may just involve having args and kwargs keys in signature. I am still not sure how robust the above is.
Even better, use the builtin functools.lru_cache which does what you want.
Regarding threading, you have the same dangers as anytime multiple threads access the same array. There is nothing special about function attributes. lru_cache should be safe (there was one bug that was resolved) with the one caveat:
To help measure the effectiveness of the cache and tune the maxsize
parameter, the wrapped function is instrumented with a cache_info()
function that returns a named tuple showing hits, misses, maxsize and
currsize. In a multi-threaded environment, the hits and misses are
approximate
functools.lru_cachecan achieve the same thing.