2

In the simplest case, I have some code where the user may want to do one thing (a), another thing, (b), or both (a+b).

The options are reasonably complex and have their own functions, but I would like an elegant way of executing one, the other, or multiple.

I want to consider the types of the choices supplied, as I think this could add unnecessary complexity when expanding to more options.

Dictionaries:

def execute(choices: List[str]):
    options = {'a', a, 'b': b}

    for choice in choices:
        options[choice]()

# could also be empty, or contain only 'a' or 'b':
choices = ['a', 'b']
execute(choices)

Enums:

class Choice(Enum):
    A = auto()
    B = auto()
    BOTH = auto()

def execute(choice: Choice):
    if choice.name == 'A':
        a()
    elif choice.name == 'B':
        b()
    elif choice.name == 'BOTH':
        a()
        b()

Keyword argument options:

def execute(*, a=False, b=False):
    if a:
        a()
    if b:
        b()

Combining dicts and enums:

class Choice(Enum):
    A = auto()
    B = auto()

def execute(choices: List[Choice]):
    options = {
        'A': a,
        'B': b,
    }

    for choice in choices:
        options[choice.name]()

I think the last option will scale the best. Short of supplying a config file, am I missing something? I can use Python 3.10 so perhaps match case could be of use.

Are there considerations to be made for extended cases e.g. "cannot perform option C when performing option A", perhaps an option validation function as well?

Please consider typing and scalability.

3
  • Why not make the options a bitmask, so they can be combined arbitrarily. For example: github.com/python/cpython/blob/…
    – jonrsharpe
    Dec 21, 2022 at 12:00
  • 3
    Looks like a strategy pattern would be the way to go to handle this
    – NeeL
    Dec 22, 2022 at 12:36
  • 2
    Strategy pattern would put you on the path to SOLID. Dec 22, 2022 at 16:09

1 Answer 1

3

Sounds like a Flag is what you want:

from enum import Flag

class Choice(Flag):
    #
    A = auto()
    B = auto()
    C = auto()
    #
    def is_valid(self):
        if Choice.A in self and Choice.C in self:
            return False
        return True

def execute(choices):
    #
    options = {
            Choice.A: do_a,
            Choice.B: do_b,
            }
    #
    if not choices.is_valid():
        raise Exception(f'Invalid choice: {choices}')
    #
    for choice in choices:
        options[choice]()

Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

3
  • This doesn't work, what is the type of the argument choices here? You treat it as both an enum, and a list to be iterated over. Dec 22, 2022 at 11:09
  • @QuantumChris: As the author of Python's enum, I assure you it does work. (Just to make sure, I added the do_x functions and tested. ;) I hate when I make typos.) Dec 22, 2022 at 16:50
  • 1
    Oh, to answer your question: the argument is a Choice such as Choice.A or Choice.A|Choice.C. If you want to accept integers as well, add a choices = Choice(choices) in execute(). Dec 22, 2022 at 16:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.