Python Tips: Dynamic function definition
In Python, there isn't any syntactic sugar that eases function definition during runtime. However, that doesn't mean that its impossible or even difficult.
from types import FunctionType
foo_code = compile('def foo(): return "bar"', "<string>", "exec")
foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")
print(foo_func())
Output
bar
Dissection
Stepping through the code line-by-line reveals how thin the language / interpreter barrier really is.
>>> from types import FunctionType
The Python docs often do not list the signatures of classes that aren't meant to be created manually (which is completely reasonable).
There are three ways to work around this: help()
, inspect
(which can't examine built-in functions), and the fallback solution of looking at the CPython source code.
In this instance both help()
and inspect
do the job, but looking at the actual source code reveals additional details in regards to data-types.
>>> from inspect import signature
>>> signature(FunctionType)
<Signature (code, globals, name=None, argdefs=None, closure=None)>
code
Internally aPyCodeObject
, which is exposed as atypes.CodeType
.
Non-built-in functions have a__code__
attribute which holds their corresponding code object.
types.CodeType
objects can be created at runtime by utilizing thecompile()
built-in.globals
If a variable that is being referenced in a function isn't defined locally, passed in as a parameter, provided by a default argument value, or supplied through a closure context, it is looked up in theglobals
dictionary.
Theglobals()
built-in function returns a reference to the global symbol table of the current module, and can therefor be used to supply a dictionary that is always up-to-date with the current state of said table. Passing in any other dictionary works as well (FunctionType((lambda: bar).__code__, {"bar" : "baz"}, "foo")() == "baz"
).name
(optional)
Controls the__name__
attribute of the returned function. Only really useful for lambdas (due to their anonymous nature they normally don't have names), and renaming functions.argdefs
(optional)
Provides a way to supply default argument values (def foo(bar="baz")
) by passing in atuple
that contains objects of arbitrary type. (FunctionType((lambda bar: bar).__code__, {}, "foo", (10,))() == 10
).closure
(optional)
(Probably shouldn't be touched if execution in any Python VM other than CPython (PyPy, Jython, ...) is desired, as it heavily relies on implementation details)
Atuple
ofcell
objects. Creatingcell
objects isn't exactly straight forward, because calling into CPython internals is required, but there is a library that makes this more convenient: exalt (shameless advertisement).
>>> foo_code = compile('def foo(): return "bar"', "<string>", "exec")
compile()
is a built-in function, and therefor also well documented.
exec
mode is utilized, because multiple statements are necessary to define a function.
>>> foo_func = FunctionType(foo_code.co_consts[0], globals(), "foo")
Bringing it all together and assigning the dynamically created function to a variable.
The function that was compiled in the previous statement becomes the first constant of the generated code object, therefor just referring to foo_code
is insufficient. This is a direct consequence of exec
mode, because the resulting code object can contain multiple constants.
>>> print(foo_func())
The dynamically created function can be called just like any other function.
Conclusion
- There are very few use-cases for dynamic function creation outside of experimentation.
- Toying around with Python internals is a great way to learn more about the language.
- The interpreter / language boundary can be crossed rather effortlessly if desired.
And as always: Don’t abuse the language (well, a little can’t hurt, right?)