I've been tinkering with decorators lately and (as an academic exercise) tried to implement a decorator that allows for partial application and/or currying of the decorated function. Furthermore this decorator should be optionally parameterizable and take a kwarg asap
which determines if the decorated function should return as soon as all mandatory args/kwargs are aquired (default: asap=True
) or if the decoratored function should keep caching args/kwargs until the function is called without arguments (asap=False
).
Here is the decorator I came up with:
def partialcurry(_f=None, *, asap: bool=True):
""" Decorator; optionally parameterizable; Allows partial application /and/or/ currying of the decorated function F. Decorated F fires as soon as all mandatory args and kwargs are supplied, or, if ASAP=False, collects args and kwargs and fires only if F is called without args/kwargs. """
def _decor(f, *args, **kwargs):
_all_args, _all_kwargs = list(args), kwargs
@functools.wraps(f)
def _wrapper(*more_args, **more_kwargs):
nonlocal _all_args, _all_kwargs # needed for resetting, not mutating
_all_args.extend(more_args)
_all_kwargs.update(more_kwargs)
if asap:
try:
result = f(*_all_args, **_all_kwargs)
# reset closured args/kwargs caches
_all_args, _all_kwargs = list(), dict()
except TypeError:
result = _wrapper
return result
elif not asap:
if more_args or more_kwargs:
return _wrapper
else:
result = f(*_all_args, **_all_kwargs)
# again, reset closured args/kwargs caches
_all_args, _all_kwargs = list(), dict()
return result
return _wrapper
if _f is None:
return _decor
return _decor(_f)
### examples
@partialcurry
def fun(x, y, z=3):
return x, y, z
print(fun(1)) # preloaded function object
print(fun(1, 2)) # all mandatory args supplied; (1,1,2); reset
print(fun(1)(2)) # all mandatory args supplied; (1,2,3); reset
print()
@partialcurry(asap=False)
def fun2(x, y, z=3):
return x, y, z
print(fun2(1)(2, 3)) # all mandatory args supplied; preloaded function object
print(fun2()) # fire + reset
print(fun2(1)(2)) # all mandatory args supplied; preloaded function object
print(fun2(4)()) # load one more and fire + reset
I am sure that this can be generally improved (implementing this as a class would be a good idea for example) and any suggestions are much appreciated, my main question however is how to determine if all mandatory args/kwargs are supplied, because I feel like to check for a TypeError
is too generic and could catch all kinds of TypeError
s. One idea would be to define a helper function that calculates the number of mandatory arguments, maybe something like this:
def _required_args_cnt(f):
""" Auxiliary function: Calculate the number of /required/ args of a function F. """
all_args_cnt = f.__code__.co_argcount + f.__code__.co_kwonlyargcount
def_args_cnt = len(f.__defaults__) if f.__defaults__ else 0
return all_args_cnt - def_args_cnt
Obviously unsatisfactory..
Any suggestions are much appreciated!