Maybe I’m dancing on my own here but I found this to be a fun exercise. Solving the general case has all kinds of complicated issues, but solving the specific case of “I want my function to default to a fresh mutable object” seems pretty useful and I think it can be solved with inspect.signature and functools.wraps. This is just a handy utility to replace if param is None: ... boilerplate.
import inspect
from functools import wraps
def mutable_default(default_factory=None, **kwargs):
"""
Wrap a function such that default None will be replaced with a new object (defaultdict semantics).
Use kwargs to define multiple factories based on the parameter name. If default_factory=None
then unspecified parameters with default None will remain as None
"""
def wrapper(func):
sig = inspect.signature(func)
none_args = set(kwargs)
if default_factory is not None:
none_args |= {kw for kw, v in sig.parameters.items() if v.default is None}
@wraps(func)
def wrapped_func(*args, **kwds):
bound_args = sig.bind(*args, **kwds)
unbound = none_args.difference(bound_args.arguments)
bound_args.apply_defaults()
bound_args.arguments.update(
(kw, kwargs.get(kw, default_factory)()) for kw in unbound
)
return func(*bound_args.args, **bound_args.kwargs)
return wrapped_func
return wrapper
There’s a little overhead with this, but if the function is doing real work it might be negligible [1]. I’ve tested this a bit but I probably haven’t covered every way to call a function.
earlier iterations were faster but covered fewer use cases ↩︎