Reputation: 534
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.
Upvotes: -2
Views: 514
Reputation: 9170
You can change the signature of a function by replacing the __code__
property value, e.g. using the convenient replace
method:
def f(a, b, c):
return (a, b, c)
f.__code__ = f.__code__.replace(co_varnames=f.__code__.co_varnames + ('d',), co_nlocals=4, co_argcount=4) # Add the `d` parameter and adjust related property values correspondingly
...but there are major caveats doing so and expecting a generally working function. You'd be surprised to find the function "broken", especially if you're not familiar with Python (CPython) bytecode.
(Everything that follows applies to the CPython implementation of Python, since the behaviour is implementation-dependent.)
Name expressions (e.g. a
in return a;
) in [compiled] functions address the variable by [a number] and not by its name! This number does not change after compilation and refers to the order of the variable in the list of "locals" (which includes the list of arguments, the way CPython implements it).
When you change the signature, adding or otherwise "reshuffling" elements that are part of the list, the same index ends up pointing to a different variable!
This can be demonstrated if you modify the function by inserting a parameter into the set of function parameters specifically at the front:
def f(a, b, c):
return (a, b, c)
f.__code__ = f.__code__.replace(co_varnames=('d',) + f.__code__.co_varnames, co_nlocals=4, co_argcount=4) # Observe that `d` is added _before_ the rest of the original set of parameters, in contrast to the expression in the first snippet
Now where with original definition of f
the call f('A', 'B', 'C')
would return the tuple ('A', 'B', 'C')
, the call to modified f
, f('D', 'A', 'B', 'C')
, will return the tuple... ('D', 'A', 'B')
! f
accesses arguments by the same index in the ordered list of parameters, that was originally computed when f
was compiled!
This is why modifying the signature with __code__
is neither most useful in this scenario nor, well, "safe".
If you use the dis
module and the eponymous dis
function it exports, you can see the disassembly showing the wrong, "shifted" mapping of indices to names for modified f
(as per the last snippet):
>>> dis.dis(f)
1 0 RESUME 0
2 2 LOAD_FAST 0 (d)
4 LOAD_FAST 1 (a)
6 LOAD_FAST 2 (b)
8 BUILD_TUPLE 3
10 RETURN_VALUE
(the three consecutive LOAD_FAST
instructions fetch d
, a
, and b
respectively, although the compiled source expressed "return a
, b
, and c
")
So in conclusion, sure -- you can modify the signature of a function -- but know what to expect.
locals()['a']
will work (and so will inspect
) regardless of where a
ends up in the [modified] parameter set, probably because the locals
built-in has a way of "locating" the variable by name in a manner that doesn't break with changes of parameter set. It's just that the name expressions break, at least.
The same general addressing problem also concerns use of free variables (e.g. those in enclosing closure), by the way.
P.S. Why all the trouble of modifying function arguments in the first place? Well, it's a [problematic] alternative to compiling new function (from source code or AST), which may be used to write decorators, at least.
Upvotes: 1
Reputation: 534
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.
Upvotes: 0