1

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?
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
frans
  • 8,868
  • 11
  • 58
  • 132
  • So you basically want to implement polymorphic methods in Python? – jonrsharpe Mar 10 '16 at 09:56
  • It would be enough for me to have an elegant way to check and communicate some constraints. Python already checks consistency for the case that you provide a *fixed number* of arguments (and it checks the *maximum* number of arguments as well). But indeed this at least comes very close to polymorphic methods/functions. – frans Mar 10 '16 at 10:10
  • You could implement something with a decorator to do this (dispatching from a single name to multiple implementations), but that likely wouldn't play nice with autocomplete etc., as the method you're actually calling would have the arbitrary `*args, **kwargs` interface. – jonrsharpe Mar 10 '16 at 10:12

1 Answers1

1

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'})
Stuart
  • 9,597
  • 1
  • 21
  • 30
  • This is nice because you still have the composite signature (which might be required by the API definition) and the explicit specialization while the boilerplate code is out-sourced (very punny :)) – frans Mar 10 '16 at 12:16
  • did this work for you? I had to subtract `self` from the argspec-`set` – frans Mar 10 '16 at 12:51
  • If `None` doesn't make a suitable default, you could just create your own object (e.g. `_DEFAULT=object()`, and `def my_func(x=_DEFAULT)`) which would fulfill the same function but without the risk of having other meanings. – acdr Mar 10 '16 at 12:58
  • @frans you're right, I have adjusted now so that `self` is in `provided` but gets removed before passing the parameters to the function. – Stuart Mar 10 '16 at 14:59