I was interested in creating functions without having to manually define them. This is useful if we want to automatically create a large number of functions, for instance inside a for loop and using a builder function. Unfortunately I wasn't able to find a canonical example of this.
This should cover everything need for the dynamic creation.
Let's say we want to create a collection of functions called add_to_1
, add_to_2
, add_to_3
, etc. Obviously you could simply do:
def add_to_1(x, y):
"add your input to 1"
return 1 + x + y
def add_to_2(x, y):
"add your input to 2"
return 2 + x + y
def add_to_3(x, y):
"add your input to 3"
return 3 + x + y
def add_to_4(x, y):
"add your input to 4"
return 4 + x + y
Obviously this gets tedious if you want to create many more. You can use exec()
, but i wanted to avoid that function.
Let's instead take a look at a builder function:
def build_fn(a):
"your docstring here"
def aux(x, y):
return a + x + y
Ok, now it's much more compact:
add_to_1 = build_fn(1)
add_to_2 = build_fn(2)
add_to_3 = build_fn(3)
add_to_4 = build_fn(4)
# ...
Read more about
locals()
,globals()
in this thread
One cool function in python is globals()
, which lets you access the dictionary of variables in your module. So the content above can be further simplified by doing this:
for i in range(5):
name = f"add_to_{i}"
fn = build_fn(i) # create the function
globals()[name] = fn # add the function to the module
If you print globals()
at this point, you will notice that the new variables have been added. They can be immediately used, and if you import your module from another file, you should be able to easily access the functions.
Ok so if you enter help(my_module)
(assuming your file is called my_module.py
) at this point you'll get:
FUNCTIONS
add_to_0 = aux(x, y)
add_to_1 = aux(x, y)
add_to_2 = aux(x, y)
add_to_3 = aux(x, y)
build_fn(a)
your docstring here
That's not optimal because you might want to have a nice doc string with a nice function name instead. So what you can do is to update the following attributes:
__docs__
(which will change the doc string),__name__
(which will change the name that appears when you runhelp
) and__qualname__
(which changes the name displayed when you print your function).
To do this, modify the for loop above in the following way:
for i in range(5):
name = f"add_to_{i}"
fn = build_fn(i) # create the function
fn.__docs__ = f"add your input to {i}" # new docstring
fn.__name__ = name # new function name
fn.__qualname__ = name # new name shown when you print
globals()[name] = fn # add the function to the module
Let's see what help
will give us now:
FUNCTIONS
add_to_0(x, y)
add your input to 0
add_to_1(x, y)
add your input to 1
add_to_2(x, y)
add your input to 2
add_to_3(x, y)
add your input to 3
add_to_4(x, y)
add your input to 4
build_fn(a)
your docstring here
fn = add_to_4(x, y)
add your input to 4
DATA
i = 4
name = 'add_to_4'
Cool, that's much better!
This thread helped me understand how to use
inspect
for changing the signature and parts were used here. Check it out!
Now, we might want to have a different function signature. What if we want a and b instead of x and y?
We'd want to create a function called __change_sig(f, new_names)
, such that when it's called, it will update function f
to the new_names
. Our previous for loop would look like this:
for i in range(5):
name = f"add_to_{i}"
fn = build_fn(i) # create the function
fn.__doc__ = f"add your input to {i}" # new docstring
fn.__name__ = name # new function name
fn.__qualname__ = name # new name shown when you print
__change_sig(fn, ['a', 'b']) # change the signature
globals()[name] = fn # add the function to the module
Now, let's see how you can build __change_sig
.
First, you will need to use inspect
to get the signature and create a new Parameter
objects.
To access the signature and replace it with new parameters:
from inspect import signature, Parameter
# just an example, you can remove this line later
f = add_to_1
# Get the signature
sig = signature(f)
f.__signature__ = sig.replace(parameters=new_params)
So that's a known way to get the signature and replace it with new parameters. But how do we get the new_params
?
You can create a param this way:
p = Param(name='a', kind=Parameter.POSITIONAL_OR_KEYWORD)
You can even include default and annotation:
p = Param(name='a', kind=Parameter.POSITIONAL_OR_KEYWORD, default=0, annotation=int)
But since you have multiple parameters, you will need new_params
to be a list when passing it to sig.replace()
:
new_params = [
Parameter(
n,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=0,
annotation=int
)
for n in new_names
]
Now, let's see the full function:
def __change_sig(f, new_names):
sig = signature(f)
new_params = [
Parameter(
n,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=0,
annotation=int
)
for n in new_names
]
f.__signature__ = sig.replace(
parameters=new_params
)
return f
The return
is optional since it modifies f
in place.
Let's see what help
will give us now:
FUNCTIONS
add_to_0(x: int = 0, y: int = 0)
add your input to 0
add_to_1(x: int = 0, y: int = 0)
add your input to 1
add_to_2(x: int = 0, y: int = 0)
add your input to 2
add_to_3(x: int = 0, y: int = 0)
add your input to 3
add_to_4(x: int = 0, y: int = 0)
add your input to 4
build_fn(a)
your docstring here
fn = add_to_4(x: int = 0, y: int = 0)
add your input to 4
DATA
i = 4
name = 'add_to_4'
You might notice that fn
and some of the data
gets returned, as well as build_fn
which you might want to hide. You can simply wrap the for loop inside a function
and name any function with name
to start with a double underscore (__
). So __change_sig
instead of change_sig
, and the following function creation/call:
def __create_functions():
def build_fn(a):
"your docstring here"
def aux(x, y):
return a + x + y
return aux
for i in range(5):
name = f"add_to_{i}"
fn = build_fn(i) # create the function
fn.__doc__ = f"add your input to {i}" # new docstring
fn.__name__ = name # new function name
fn.__qualname__ = name # new name shown when you print
__change_sig(fn, ['x', 'y']) # change the signature
globals()[name] = fn # add the function to the module
__create_functions()
Which would yield the following:
FUNCTIONS
add_to_0(a: int = 0, b: int = 0)
add your input to 0
add_to_1(a: int = 0, b: int = 0)
add your input to 1
add_to_2(a: int = 0, b: int = 0)
add your input to 2
add_to_3(a: int = 0, b: int = 0)
add your input to 3
add_to_4(a: int = 0, b: int = 0)
add your input to 4
Here's the final module:
from inspect import signature, Parameter
def __change_sig(f, new_names):
sig = signature(f)
new_params = [
Parameter(
n,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=0,
annotation=int
)
for n in new_names
]
f.__signature__ = sig.replace(
parameters=new_params
)
return f
def __create_functions():
def build_fn(a):
"your docstring here"
def aux(x, y):
return a + x + y
return aux
for i in range(5):
name = f"add_to_{i}"
fn = build_fn(i) # create the function
fn.__doc__ = f"add your input to {i}" # new docstring
fn.__name__ = name # new function name
fn.__qualname__ = name # new name shown when you print
__change_sig(fn, ['a', 'b']) # change the signature
globals()[name] = fn # add the function to the module
__create_functions()
Thanks, this is super helpful! 🥇
About type hint annotations: you can alternately modify them via
__annotations__
without changing the function signature: