Search code examples
pythonkeyword-argument

How to call function with dict, while ignoring unexpected keyword arguments?


Something of the following sort. Imagine this case:

def some_function(a, b):
    return a + b
  
some_magical_workaround({"a": 1, "b": 2, "c": 3})  # returns 3

I can't modify some_function to add a **kwargs parameter. How could I create a wrapper function some_magical_workaround which calls some_function as shown?

Also, some_magical_workaround may be used with other functions, and I don't know beforehand what args are defined in the functions being used.


Solution

  • So, you cannot do this in general if the function isn't written in Python (e.g. many built-ins, functions from third-party libraries written as extensions in C) but you can use the inpsect module to introspect the signature. Here is a quick-and-dirty proof-of-concept, I haven't really considered edge-cases, but this should get you going:

    import inspect
    
    def bind_exact_args(func, kwargs):
        sig = inspect.signature(func) # will fail with functions not written in Python, e.g. many built-ins
        common_keys = sig.parameters.keys() & kwargs.keys()
        return func(**{k:kwargs[k] for k in common_keys})
    
    def some_function(a, b):
        return a + b
    
    

    So, a demonstration:

    >>> import inspect
    >>>
    >>> def bind_exact_args(func, kwargs):
    ...     sig = inspect.signature(func) # will fail with functions not written in Python, e.g. many built-ins
    ...     return func(**{k:kwargs[k] for k in sig.parameters.keys() & kwargs.keys()})
    ...
    >>> def some_function(a, b):
    ...     return a + b
    ...
    >>> bind_exact_args(some_function, {"a": 1, "b": 2, "c": 3})
    3
    

    But note how it can fail with built-ins:

    >>> bind_exact_args(max, {"a": 1, "b": 2, "c": 3})
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 2, in bind_exact_args
      File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3113, in signature
        return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
      File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2862, in from_callable
        return _signature_from_callable(obj, sigcls=cls,
      File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2329, in _signature_from_callable
        return _signature_from_builtin(sigcls, obj,
      File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2147, in _signature_from_builtin
        raise ValueError("no signature found for builtin {!r}".format(func))
    ValueError: no signature found for builtin <built-in function max>
    

    As noted in @JamieDoornbos answer, another example that will not work is a function with positional-only paramters:

    E.g.:

    def some_function(a, b, /, c):
        return a + b + c
    

    Although, you can introspect this:

    >>> def some_function(a, b, /, c):
    ...     return a + b + c
    ...
    >>> sig = inspect.signature(some_function)
    >>> sig.parameters['a'].kind
    <_ParameterKind.POSITIONAL_ONLY: 0>
    >>> sig.parameters['b'].kind
    <_ParameterKind.POSITIONAL_ONLY: 0>
    >>> sig.parameters['c'].kind
    <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>
    

    If you need to handle this case, it is certainly possible to, but I leave that as an exercise to the reader :)