pythonpython-decorators

How to add function parameter kwargs during runtime?


I have hundreds of functions test1, test2, ...

Function testX may or may not have kwargs:

def test1(x, z=1, **kwargs):
    pass
def test2(x, y=1):
    pass
def test3(x, y=1, z=1):
    pass
...

and I have a function call_center:

tests = [test1, test2, test3]
def call_center(**kwargs):
    for test in tests:
        test(**kwargs)

call_center will be called with various input parameters:

call_center(x=1,y=1)
call_center(x=1,z=1)

I don't want to filter kwargs by inspect.signature of the called function, because these function will be called millions of times, filtering at runtime costs a lot.

How can I define a decorator extend_kwargs that adds kwargs to functions that do not have kwargs?

I can use signature to find if kwargs exists.

def extender(func):
    sig = signature(func)
    params = list(sig.parameters.values())
    if any(
        param.name
        for param in sig.parameters.values()
        if param.kind == param.VAR_KEYWORD
    ):
        return func
    # add kwargs to func here
    ...
    return func

and i tried add Parameter to signature like this

    kwargs = Parameter("__auto_kwargs", Parameter.VAR_KEYWORD)
    params.append(kwargs)
    new_sig = sig.replace(parameters=params)
    func.__signature__ = new_sig

It seems signature is just signature, does not affect the execution.

and i tried to modify __code__ of func.

    old_code = func.__code__
    code = old_code.replace(
        co_varnames=old_code.co_varnames + ("__auto_kwargs",),
        co_nlocals=old_code.co_nlocals + 1,
    )
    func.__code__ = code

Still not work

Refer to the accepted answer, the following code works.

The judgment is simplified and add the modification of co_flags.

def extender(func):
    if not func.__code__.co_flags & inspect.CO_VARKEYWORDS:
        code = func.__code__
        func.__code__ = code.replace(
            co_flags=code.co_flags | inspect.CO_VARKEYWORDS,
            co_varnames=code.co_varnames + ("kwargs",),
            co_nlocals=code.co_nlocals + 1,
        )
    return func

If you don't want to modify the original function directly, see the accepted answer


Solution

  • To add variable keyword parameters (kwargs) to a function that does not have it you can re-create the function with types.FunctionType but with the code object replaced with one that has the inspect.CO_VARKEYWORDS flag enabled in the co_flags attribute, the kwargs name added to the co_varnames attribute, and the number of local variables incremented by 1 in the co_nlocals attribute.

    So a decorator that adds kwargs to functions that do not have it can look like:

    import inspect
    from types import FunctionType
    
    def ensure_kwargs(func):
        if func.__code__.co_flags & inspect.CO_VARKEYWORDS:
            return func # already supports kwargs
        return FunctionType(
            code=func.__code__.replace(
                co_flags=func.__code__.co_flags | inspect.CO_VARKEYWORDS,
                co_varnames=func.__code__.co_varnames + ('kwargs',),
                co_nlocals=func.__code__.co_nlocals + 1
            ),
            globals=func.__globals__,
            name=func.__name__,
            argdefs=func.__defaults__,
            closure=func.__closure__
        )
    

    so that:

    def test1(x, z=1, **kwargs):
        print(f'{x=}, {z=}, {kwargs=}')
    def test2(x, y=1):
        print(f'{x=}, {y=}')
    def test3(x, y=1, z=1):
        print(f'{x=}, {y=}, {z=}')
    
    def call_center(**kwargs):
        for test in tests:
            test(**kwargs)
    tests = list(map(ensure_kwargs, [test1, test2, test3]))
    call_center(x=1, y=2)
    call_center(x=3, z=4) 
    

    outputs:

    x=1, z=1, kwargs={'y': 2}
    x=1, y=2
    x=1, y=2, z=1
    x=3, z=4, kwargs={}
    x=3, y=1
    x=3, y=1, z=4
    

    Demo: https://ideone.com/0aeT2m