Pythonic way to have a choice of 2-3 options as an argument to a function

前端 未结 6 794
抹茶落季
抹茶落季 2021-02-07 07:16

I have a Python function which requires a number of parameters, one of which is the type of simulation to perform. For example, the options could be \"solar\", \"view\" or \"bot

相关标签:
6条回答
  • 2021-02-07 07:22

    I don't like any of those options.

    I'd define two different functions, perform_solar(a, b, c) and perform_view(a, b, c) and let the caller decide which ones he wants to use, in which order and with which arguments.

    If the reason why you thought you'd have to pack these into one single function is that they share state, you should share that state in an object and define the functions as methods.

    0 讨论(0)
  • 2021-02-07 07:26

    If the point Niklas' makes in his answer doesn't hold, I would use a string argument. There are Python modules in the standard library that use similar arguments. For example csv.reader().

    sim_func(a, b, c, sim_type='solar')
    

    Remember to give a reasonable error inside the function, that helps people out if they type in the wrong thing.

    def sim_func(a, b, c, sim_type='solar'):
        sim_types = ['solar', 'view', 'both']
        if sim_type not in sim_types:
            raise ValueError("Invalid sim type. Expected one of: %s" % sim_types)
        ...
    
    0 讨论(0)
  • 2021-02-07 07:28

    Since functions are objects in python, you could actually process *args as a list of methods and pass the types of simulations as arbitratry args at the end. This would have the benefit of allowing you to define new simulations in the future without having to refactor this code.

    def func(a, b, c, *args):
        for arg in args:
            arg(a, b, c)
    
    def foosim(a, b, c):
        print 'foosim %d' % (a + b + c)
    
    def barsim(a, b, c):
        print 'barsim %d' % (a * b * c)
    

    Use:

    func(2, 2, 3, foosim)
    func(2, 2, 3, barsim)
    func(2, 2, 3, foosim, barsim)
    

    Output:

    foosim 7
    barsim 12
    foosim 7
    barsim 12
    
    0 讨论(0)
  • 2021-02-07 07:33

    You can use optional (keyword) arguments like this

    def func(a, b, c, **kw):
        if kw.get('do_solar'):
            # Do Solar
        if kw.get('do_view'):
            # Do view
    
    0 讨论(0)
  • 2021-02-07 07:46

    Just have written a decorator factory for this, based on your option #1, which is "Use a string variable and check it".

    def limited_argument_choices(choices: Dict[int or str, Iterable] = None) -> Callable:
        """decorator factory: force arguments of a func limited in the given choices
    
        :param choices: a dict which describes the choices for the value-limited arguments.
                the key of the dict must be either the index of args or the key_str of kwargs,
                while the value of the dict must be an iterable."""
        err_fmt = "value of '{}' is not a valid choice: '{}'"
    
        def decorator(func):
            if not choices:
                return func
    
            @wraps(func)
            def decorated_func(*args, **kwargs):
                for i in range(len(args)):
                    if i in choices and args[i] not in choices[i]:
                        param_name = func.__code__.co_varnames[i]
                        valid_choices = list(choices[i])
                        raise ValueError(err_fmt.format(param_name, valid_choices))
                for k in kwargs:
                    if k in choices and kwargs[k] not in choices[k]:
                        raise ValueError(err_fmt.format(k, list(choices[k])))
    
                return func(*args, **kwargs)
    
            return decorated_func
    
        return decorator
    

    So now we could make new functions like this:

    @limited_argument_choices({1: (0, 1, 2), 'y': ('hi', 'hello')})
    def test(a, b, c, y=1):
        print(a, b, c, y)
    

    And test it:

    test(0, 1, 2, y='hello')
    test(0, 3, 2, y='hello')
    test(0, 1, 2, y='world')
    

    Output:

    0 1 2 hello
    ValueError: value of 'b' is not a valid choice: '[0, 1, 2]'
    ValueError: value of 'y' is not a valid choice: '['hi', 'hello']'
    

    This decorator still needs improvements, but it's already usable now.


    An improved revision here:

    def decorator_factory_args_choices(choices: Dict[int or str, Iterable]) -> Decorator:
        """decorator factory: force arguments of a func limited inside the given choices
    
        :param choices: a dict which describes the choices of arguments
            the key of the dict must be either the index of args or the key(str) of kwargs
            the value of the dict must be an iterable."""
        err_fmt = "value of '{}' is not a valid choice in {}"
    
        def decorator(func):
            @wraps(func)
            def decorated_func(*args, **kwargs):
                for arg_index in range(len(args)):
                    param_name = func.__code__.co_varnames[arg_index]
                    if arg_index in choices and args[arg_index] not in choices[arg_index]:
                        raise ValueError(err_fmt.format(param_name, choices[arg_index]))
                    elif param_name in choices and args[arg_index] not in choices[param_name]:
                        raise ValueError(err_fmt.format(param_name, choices[param_name]))
                for param_name in kwargs:
                    if param_name in choices and kwargs[param_name] not in choices[param_name]:
                        raise ValueError(err_fmt.format(param_name, choices[param_name]))
    
                return func(*args, **kwargs)
    
            return decorated_func
    
        return decorator
    
    0 讨论(0)
  • 2021-02-07 07:47

    You can use the assert statement like this:

    assert sim_types in ['solar', 'view', 'both'], 'sim type parameter must be solar, view or both'
    

    If sim_types is not in the list, python will raise an Assertion Error

    0 讨论(0)
提交回复
热议问题