I would like to define a function make_function
that returns a new function. It takes as arguments a list arg_names
of argument names for the new function and a function inner_func
to be used in the definition of the new function.
The new function will just add 5 to the output of inner_func
in this simple case.
I tried using eval()
:
def make_function(args_names: List[str], inner_func: Callable):
args_str = str.join(", ", args_names)
expr: str = (
"lambda "
args_str
": "
inner_func.__name__
"("
args_str
") 5"
)
return eval(expr)
This works.
But using eval
is not recommended. For once, it is not easy to debug.
Also, in my use case I need to call the new function from a place where inner_func
is not available, which raises an error.
Any other options?
CodePudding user response:
Perhaps exec
is actually the best approach. eval
/exec
are usually not recommended if the input string is arbitrary/untrusted. However, when the code string which is eventually passed to the compiler was also generated by you, and not directly supplied by user, then it can be fine. There are some stdlib examples using this approach: namedtuple
uses eval
and dataclass
uses exec
, nobody has figured out how to exploit them (yet).
Now, I think that in your case it's fairly easy to do a safe code generation exec simply by verifying that the args_names
passed in is truly a list of arg names, and not some arbitrary Python code.
from textwrap import dedent
from typing import List, Callable
def make_function(args_names: List[str], inner_func: Callable):
for arg_name in args_names:
if not arg_name.isidentifier():
raise Exception(f"Invalid arg name: {arg_name}")
args = ', '.join(args_names)
code_str = dedent(f"""\
def outer_func({args}):
return inner_func({args}) 5
""")
scope = {"inner_func": inner_func}
exec(code_str, scope)
return scope["outer_func"]
Demo:
>>> def orig(a, b):
... return a b 1
...
>>> func = make_function(args_names=["foo", "bar"], inner_func=orig)
>>> func(2, 3)
11
>>> func(foo=2, bar=3)
11
>>> func(foo=2, bar=3, baz=4)
TypeError: outer_func() got an unexpected keyword argument 'baz'
>>> func(foo=2)
TypeError: outer_func() missing 1 required positional argument: 'bar'
As desired, it continues to work even when a local reference to the inner_func
is no longer available, since we made sure the reference was available during code gen:
>>> del orig
>>> func(foo=2, bar=3)
11
Nefarious "argument names" are not allowed:
>>> make_function(["foo", "bar", "__import__('os')"], orig)
Exception: Invalid arg name: __import__('os')
For an approach without using code generation, it is also possible to instantiate types.FunctionType
directly. To do this you need to pass it a types.CodeType
instance, which are pretty difficult to create manually. These are public/documented, but the docstring for the code type even tries to scare you away:
>>> ((lambda: None).__code__.__doc__)
'Create a code object. Not for the faint of heart.'
If you want to attempt it regardless, see How to create a code object in python? but I think you'll find that using eval
, exec
or compile
is more convincing.