0

I struggled to formulate the question, due to the lack of terminology on my part. I am not asking about keyword arguments with default value. My question is about how to handle argument values that can only have a set of values, and how to optimise for code readability and prevent repetitions.

See this function for example:

def foo(opt='something'):

   if opt == 'something':
      ret = dowhatever()

   elif opt == 'somethingelse':
      ret = dowhateverelse()

   else:
      raise UnrecognisedArgumentException(opt)

return ret

In my opinion, this is quite ugly. Its basically a not-so-good-looking java-switch translation in python. A problem that arises is when the cases have common code (repetition) in between case-associated code, which I want to avoid. If I were to avoid this I would write:

def foo(opt='something'):
   if opt not in ['something', 'something_else']:
      raise UnrecognisedArgumentException

   if opt == 'something':
      ret = do_whatever()

   elif opt == 'something_else':
      ret = do_whatever_else()

   do_something_universal(ret)

   if opt == 'something':
      ret = do_whatever_afterwards()

   elif opt == 'something_else':
      ret = do_whatever_else_afterwards()

return ret

This is even uglier. Is there a better way of writing code like this?

Snusifer
  • 485
  • 3
  • 17
  • Generally you would raise `ValueError` when a disallowed value is used. – James Nov 20 '19 at 21:42
  • I would have a dict like `{'something': dowhatever, 'somethingelse': dowhateverelse}` instead. A function that can behave arbitrarily differently based on an argument should probably be two functions in the first place. – chepner Nov 20 '19 at 21:42
  • Your second question makes a case for defining two separate context managers, where `something.__enter__ = do_whatever` and `something.__exit__ = do_whatever_afterwords`, etc. – chepner Nov 20 '19 at 21:44
  • @chepner Well yeah, but image that the code blocks were much bigger, and not a single function. The example was just to illustrate – Snusifer Nov 20 '19 at 21:44
  • @James Yes iknow, it was just to illustrate – Snusifer Nov 20 '19 at 21:46

1 Answers1

2

I'm reopening this question (previously closed as a duplicate of Making case statements in Python) in order to address the specific issue of having common code between two related case-like blocks. This relates to the idea of context managers. Here, we're defining two different context managers, and storing them in a dict; the case-statement-replacement serves to select which context manager we'll use.

import contextlib

# Step 1: Define the context managers. Usingcontextlib.contextmanager for
# simplicity, but you can define a class with `__enter__` and `__exit__`
# methods as well.

@contextlib.contextmanager
def Something():
    yield do_whatever()
    do_whatever_afterwards()


@contextlib.contextmanager
def SomethingElse():
    yield do_whatever_else()
    do_whatever_else_afterwards()


# Step 2: Map your selectors to the context managers
cms = {
    'something': Something,
    'something_else': SomethingElse
}


# Step 3: Select a context manager and handle attempts to select a non-existent one
opt = ...  # 'something' or 'something_else'
try:
    cm = cms[opt]
except KeyError:
    raise UnrecognisedArgumentException(opt)

# Step 4: Run your universal code in the correct context
with cm() as x:
    do_something_universal(x)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Yes thank you! Very nice example. I was confused because the 'duplicate' didn't have the answer i was looking for. – Snusifer Nov 20 '19 at 21:57