Search code examples
python-2.7globaldefault-parameters

Python 2.7: Defining default parameters based on globals?


I'm writing a utility where I would like to have global variables that change the way a function operates. By default I'd like all the functions to follow one style, but in certain cases I'd also like the ability to force the way a function operates.

Say I have a file Script_Defaults.py with

USER_INPUT = True

In another python file I have many functions like this:

from Script_Defaults import USER_INPUT
def DoSomething(var_of_data, user_input = USER_INPUT):
    if user_input:
        ... #some code here that asks the user what to change in var_of_data
    .... # goes on to do something

The problem I face here is that the default parameter only loads once when the file starts. I want to be able to set USER_INPUT as False or True during the run of the program. To get this behaviour I'm currently using it like this...

from Script_Defaults import USER_INPUT
def DoSomething(var_of_data, user_input = None):
    if user_input is None:
         user_input = USER_INPUT
    if user_input:
        ... #some code here that asks the user what to change in var_of_data
    .... # goes on to do something

This seems like a lot of unnecessary code, especially if I have a lot of conditions like USER_INPUT, and many functions that need them. Is there a better to get this functionality, or is this the only way?


Solution

  • Using decorators, and manipulation of a function's default arguments, you can use the following solution:

    from change_defaults import Default, set_defaults
    
    my_defaults = dict(USER_INPUT=0)
    
    
    @set_defaults(my_defaults)
    def DoSomething(var_of_data, user_input=Default("USER_INPUT")):
        return var_of_data, user_input
    
    
    def main():
        print DoSomething("This")
    
        my_defaults["USER_INPUT"] = 1
    
        print DoSomething("Thing")
    
        my_defaults["USER_INPUT"] = 2
    
        print DoSomething("Actually")
        print DoSomething("Works", 3)
    
    
    if __name__ == "__main__":
        main()
    

    Which requires the following code:

    # change_defaults.py
    
    from functools import wraps
    
    class Default(object):
        def __init__(self, name):
            super(Default, self).__init__()
    
            self.name = name
    
    
    def set_defaults(defaults):
        def decorator(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                # Backup original function defaults.
                original_defaults = f.func_defaults
    
                # Replace every `Default("...")` argument with its current value.
                function_defaults = []
                for default_value in f.func_defaults:
                    if isinstance(default_value, Default):
                        function_defaults.append(defaults[default_value.name])
                    else:
                        function_defaults.append(default_value)
    
                # Set the new function defaults.
                f.func_defaults = tuple(function_defaults)
    
                return_value = f(*args, **kwargs)
    
                # Restore original defaults (required to keep this trick working.)
                f.func_defaults = original_defaults
    
                return return_value
    
            return wrapper
    
        return decorator
    

    By defining the default parameters with Default(parameter_name) you tell the set_defaults decorator which value to take from the defaults dict.

    Also, with a little more code (irrelevant to the solution) you can make it work like:

    @set_defaults(my_defaults)
    def DoSomething(var_of_data, user_input=Default.USER_INPUT):
        ...