7
\$\begingroup\$

This is self-explaining example with usage in doctests (it's not that fast as implementation with dict key-lookups, but it's a lot more readable, and don't require callables and lambdas):

class Switch(object):
 """
 Switch, simple implementation of switch statement for Python, eg:
 >>> def test_switch(val):
 ... ret = []
 ... with Switch(val) as case:
 ... if case(1, fall_through=True):
 ... ret.append(1)
 ... if case(2):
 ... ret.append(2)
 ... if case.call(lambda v: 2 < v < 4):
 ... ret.append(3)
 ... if case.call(lambda v: 3 < v < 5, fall_through=True):
 ... ret.append(4)
 ... if case(5):
 ... ret.append(5)
 ... if case.default:
 ... ret.append(6)
 ... return ret
 ...
 >>> test_switch(1)
 [1, 2]
 >>> test_switch(2)
 [2]
 >>> test_switch(3)
 [3]
 >>> test_switch(4)
 [4, 5]
 >>> test_switch(5)
 [5]
 >>> test_switch(7)
 [6]
 >>> def test_switch_default_fall_through(val):
 ... ret = []
 ... with Switch(val, fall_through=True) as case:
 ... if case(1):
 ... ret.append(1)
 ... if case(2):
 ... ret.append(2)
 ... if case.call(lambda v: 2 < v < 4):
 ... ret.append(3)
 ... if case.call(lambda v: 3 < v < 5, fall_through=False):
 ... ret.append(4)
 ... if case(5):
 ... ret.append(5)
 ... if case.default:
 ... ret.append(6)
 ... return ret
 ...
 >>> test_switch_default_fall_through(1)
 [1, 2, 3, 4]
 >>> test_switch_default_fall_through(2)
 [2, 3, 4]
 >>> test_switch_default_fall_through(3)
 [3, 4]
 >>> test_switch_default_fall_through(4)
 [4]
 >>> test_switch_default_fall_through(5)
 [5]
 >>> test_switch_default_fall_through(7)
 [6]
 """
 class StopExecution(Exception):
 pass
 def __init__(self, test_value, fall_through=False):
 self._value = test_value
 self._fall_through = None
 self._default_fall_through = fall_through
 self._use_default = True
 self._default_used = False
 def __enter__(self):
 return self
 def __exit__(self, exc_type, exc_val, exc_tb):
 if exc_type is self.StopExecution:
 return True
 return False
 def __call__(self, expr, fall_through=None):
 return self.call(lambda v: v == expr, fall_through)
 def call(self, call, fall_through=None):
 if self._default_used:
 raise SyntaxError('Case after default is prohibited')
 if self._finished:
 raise self.StopExecution()
 elif call(self._value) or self._fall_through:
 self._use_default = False
 if fall_through is None:
 self._fall_through = self._default_fall_through
 else:
 self._fall_through = fall_through
 return True
 return False
 @property
 def default(self):
 if self._finished:
 raise self.StopExecution()
 self._default_used = True
 if self._use_default:
 return True
 return False
 @property
 def _finished(self):
 return self._use_default is False and self._fall_through is False
class CSwitch(Switch):
 """
 CSwitch is a shortcut to call Switch(test_value, fall_through=True)
 """
 def __init__(self, test_value):
 super(CSwitch, self).__init__(test_value, fall_through=True)
Toby Speight
87.1k14 gold badges104 silver badges322 bronze badges
asked Mar 30, 2014 at 12:46
\$\endgroup\$
1
  • \$\begingroup\$ If you want to preserve the case/break syntax of C, here is an interesting implementation: code.activestate.com/recipes/410692. \$\endgroup\$ Commented Apr 3, 2014 at 5:05

1 Answer 1

4
\$\begingroup\$

I like your trick to create this syntactic sugar. The implementation is also pretty good, as are the doctests.

Feature suggestions

I think it would be nice if a case() could test for multiple values. A case('jack', 'queen', 'king') should match if the Switch was created with any of those three strings.

It would also be nice if there were a case.match() that performed a regular expression match.

Minor issues

  1. If execution ends up inside case.call() due to fall-through, then I would expect the test function not to be called at all, as a kind of short-circuiting behaviour. Specifically,

    elif call(self._value) or self._fall_through:
    

    should be reversed and written as

    elif self._fall_through or call(self._value):
    
  2. Having a parameter named call when the method is also named call is confusing. I suggest renaming the parameter to test.

  3. Avoid testing variables for equality with True and False explicitly. Just use boolean expressions. For example, in __exit__(), change

    def __exit__(exc_type, exc_val, exc_tb):
     if exc_type is self.StopExecution:
     return True
     return False
    

    to

    def __exit__(exc_type, exc_val, exc_tb):
     return exc_type is self.StopExecution
    
  4. Initialize _fall_through to False instead of None; it's slightly more informative.

  5. Rename exprcase_value. Rename _value_switch_value.

  6. Rename / invert _use_default to not _matched_case, because _use_default is too confusingly similar to _default_used. Also, by inverting the logic, all three private variables can be initialized to False, which is more elegant.

  7. Instead of a _finished property, write a _check_finished() method that raises StopException too.

Proposed solution

import re
class Switch(object):
 """
 Switch, simple implementation of switch statement for Python, eg:
 >>> def test_switch(val):
 ... ret = []
 ... with Switch(val) as case:
 ... if case(1, fall_through=True):
 ... ret.append(1)
 ... if case.match('2|two'):
 ... ret.append(2)
 ... if case.call(lambda v: 2 < v < 4):
 ... ret.append(3)
 ... if case.call(lambda v: 3 < v < 5, fall_through=True):
 ... ret.append(4)
 ... if case(5, 10):
 ... ret.append('5 or 10')
 ... if case.default:
 ... ret.append(6)
 ... return ret
 ...
 >>> test_switch(1)
 [1, 2]
 >>> test_switch(2)
 [2]
 >>> test_switch(3)
 [3]
 >>> test_switch(4)
 [4, '5 or 10']
 >>> test_switch(5)
 ['5 or 10']
 >>> test_switch(10)
 ['5 or 10']
 >>> test_switch(7)
 [6]
 >>> def test_switch_default_fall_through(val):
 ... ret = []
 ... with Switch(val, fall_through=True) as case:
 ... if case(1):
 ... ret.append(1)
 ... if case(2):
 ... ret.append(2)
 ... if case.call(lambda v: 2 < v < 4):
 ... ret.append(3)
 ... if case.call(lambda v: 3 < v < 5, fall_through=False):
 ... ret.append(4)
 ... if case(5):
 ... ret.append(5)
 ... if case.default:
 ... ret.append(6)
 ... return ret
 ...
 >>> test_switch_default_fall_through(1)
 [1, 2, 3, 4]
 >>> test_switch_default_fall_through(2)
 [2, 3, 4]
 >>> test_switch_default_fall_through(3)
 [3, 4]
 >>> test_switch_default_fall_through(4)
 [4]
 >>> test_switch_default_fall_through(5)
 [5]
 >>> test_switch_default_fall_through(7)
 [6]
 """
 class StopExecution(Exception):
 pass
 def __init__(self, switch_value, fall_through=False):
 self._switch_value = switch_value
 self._default_fall_through = fall_through
 self._fall_through = False
 self._matched_case = False
 self._default_used = False
 def __enter__(self):
 return self
 def __exit__(self, exc_type, exc_val, exc_tb):
 return exc_type is self.StopExecution
 def __call__(self, case_value, *case_values, **kwargs):
 def test(switch_value):
 return any(switch_value == v for v in (case_value,) + case_values)
 return self.call(test, **kwargs)
 def call(self, test, fall_through=None):
 if self._default_used:
 raise SyntaxError('Case after default is prohibited')
 self._check_finished()
 if self._fall_through or test(self._switch_value):
 self._matched_case = True
 self._fall_through = fall_through if fall_through is not None else self._default_fall_through
 return True
 return False
 def match(self, regex, fall_through=None):
 if self._default_used:
 raise SyntaxError('Match after default is prohibited')
 self._check_finished()
 if isinstance(regex, str):
 regex = re.compile(regex)
 if self._fall_through or regex.match(str(self._switch_value)):
 self._matched_case = True
 self._fall_through = fall_through if fall_through is not None else self._default_fall_through
 return True
 return False
 @property
 def default(self):
 self._check_finished()
 self._default_used = True
 return not self._matched_case
 def _check_finished(self):
 if self._matched_case and not self._fall_through:
 raise self.StopExecution()
class CSwitch(Switch):
 """
 CSwitch is a shortcut to call Switch(switch_value, fall_through=True)
 """
 def __init__(self, switch_value):
 super(CSwitch, self).__init__(switch_value, fall_through=True)
answered Apr 12, 2014 at 5:57
\$\endgroup\$
1
  • \$\begingroup\$ Thanks, I've added also ability to have multiple regexp patterns within single case/match call, code is available on pypi: pypi.python.org/pypi/switch/1.1.0 \$\endgroup\$ Commented Apr 12, 2014 at 11:52

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.