Search code examples
pythonparameter-passingoverloadingnamed-parameters

Force specific function signatures


I just caught myself writing code like this:

def register(self, *,  # * enforces keyword-only parameters
             key_value_container:dict=None,    # legal parameter set #1
             key:str=None, value=None):        # legal parameter set #2

    # enforce one of the parameter sets
    if not ((key_value_container is not None and 
             key is None and value is None) or
            (key is not None and value is not None and 
             key_value_container is None)):
        raise TypeError('provide (key_value_container) or (key/value')

    # handle each legal parameter setf
    if key_value_container is not None:
        for _k, _s in key_value_container.items():
            self.register(_k, _s)
    else:
        do_something_with(key, value)

My goal was to have two signatures of the method register(): it should take a key and a value or some container with a number of keys and values e.g. a dict.

The * in the argument at least forces me to provide named arguments but there is no need to provide a given number of arguments or even a given set of arguments.

Of course I can (should) provide two methods with different names and signatures instead of one.

But in case I want (have) to provide one method/function with more than one parameter semantics - what's the best way to make this visible and enforce it?

In detail:

  • is it possible to make clear in auto-completion and documentation which are the legal parameter combinations?
  • how do I check (with as little boilerplate code as possible) whether one combination has been provided?

Solution

  • To avoid boilerplate code you could so something like this: write additional functions that specify the different signatures. Then have a redirect helper function that sends parameters from the main function to the additional functions depending on which signature the parameters match.

    It's still not particularly elegant. If generalising this you would also need to consider how to deal with cases where None is a valid parameter you might want to pass.

    from inspect import getargspec
    class Eg(object):
        def register(self, key_value_container=None, key=None, value=None):
            return redirect(locals(), [self.register_dict, self.register_single])
    
        def register_dict(self, key_value_container=None):
            print('register dict', key_value_container)
    
        def register_single(self, key=None, value=None):
            print('register single', key, value)
    
    def redirect(params, functions):
        provided = {k for k, v in params.items() if v is not None}
        for function in functions:
            if set(getargspec(function).args) == provided:
                return function(**{k: params[k] for k in provided if k != 'self'})
        raise TypeError('wrong configuration provided') 
        # This error could be expanded to explain which signatures are allowed       
    
    a = Eg()
    a.register(key='hi', value='there')
    a.register(key_value_container={'hi': 'there'})