python-3.xargumentsabstract-syntax-treepython-internals

How to modify the signature of a function dynamically


I am writing a framework in Python. When a user declares a function, they do:

def foo(row, fetch=stuff, query=otherStuff)

def bar(row, query=stuff)

def bar2(row)

When the backend sees query= value, it executes the function with the query argument depending on value. This way the function has access to the result of something done by the backend in its scope.

Currently I build my arguments each time by checking whether query, fetch and the other items are None, and launching it with a set of args that exactly matches what the user asked for. Otherwise I got the "got an unexpected keyword argument" error. This is the code in the backend:

#fetch and query is something computed by the backend
if fetch= None and query==None:
    userfunction(row)
elif fetch==None:
    userunction (row, query=query)
elif query == None:
    userfunction (row, fetch=fetch)
else:
    userfunction (row,fetch=fetch,query=query)

This is not good; for each additional "service" the backend offers, I need to write all the combinations with the previous ones.

Instead of that I would like to primarily take the function and manually add a named parameter, before executing it, removing all the unnecessary code that does these checks. Then the user would just use the stuff it really wanted.

I don't want the user to have to modify the function by adding stuff it doesn't want (nor do I want them to specify a kwarg every time).

So I would like an example of this if this is doable, a function addNamedVar(name, function) that adds the variable name to the function function.

I want to do that that way because the users functions are called a lot of times, meaning that it would trigger me to, for example, create a dict of the named var of the function (with inspect) and then using **dict. I would really like to just modify the function once to avoid any kind of overhead.


Solution

  • This is indeed doable in AST and that's what I am gonna do because this solution will suit better for my use case . However you could do what I asked more simply by having a function cloning approach like the code snippet I show. Note that this code return the same functions with different defaults values. You can use this code as example to do whatever you want. This works for python3

    def copyTransform(f, name, **args):
        signature=inspect.signature(f)
        params= list(signature.parameters)
        numberOfParam= len(params)
        numberOfDefault= len(f.__defaults__)
        listTuple= list(f.__defaults__)
    
        for key,val in args.items():
            toChangeIndex = params.index(key, numberOfDefault)
            if toChangeIndex:
                listTuple[toChangeIndex- numberOfDefault]=val
    
        newTuple= tuple(listTuple)
        oldCode=f.__code__
    
        newCode= types.CodeType(
            oldCode.co_argcount,             #   integer
            oldCode.co_kwonlyargcount,       #   integer
            oldCode.co_nlocals,              #   integer
            oldCode.co_stacksize,            #   integer
            oldCode.co_flags,                #   integer
            oldCode.co_code,                 #   bytes
            oldCode.co_consts,               #   tuple
            oldCode.co_names,                #   tuple
            oldCode.co_varnames,             #   tuple
            oldCode.co_filename,             #   string
            name,                            #   string
            oldCode.co_firstlineno,          #   integer
            oldCode.co_lnotab,               #   bytes
            oldCode.co_freevars,             #   tuple
            oldCode.co_cellvars              #   tuple
            )
    
        newFunction=types.FunctionType(newCode, f.__globals__, name, newTuple, f.__closure__)
        newFunction.__qualname__=name #also needed for serialization
    

    You need to do that weird stuff with the names if you want to Pickle your clone function.