Revisit Mutable Default Arguments

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.


  1. earlier iterations were faster but covered fewer use cases ↩︎

4 Likes