Skip to content

Instantly share code, notes, and snippets.

@xhluca
Last active August 2, 2021 21:28
Show Gist options
  • Save xhluca/95117e225b7a1aa806e696180a72bdd0 to your computer and use it in GitHub Desktop.
Save xhluca/95117e225b7a1aa806e696180a72bdd0 to your computer and use it in GitHub Desktop.
Here's how to dynamically create functions in Python (with doc string and updated names)

How to dynamically create functions in Python

Context

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.

Example - Simple add function

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

Builder function

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)
# ...

Variable assignment by manipulating globals()

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.

Updating the doc string and var names

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 run help) 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!

Updating function signature

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'

(Optional) Some cosmetic changes

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

Putting everything together

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()
@xhluca
Copy link
Author

xhluca commented Aug 2, 2021

Cool, thanks for sharing @ctrueden

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment