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) 
\$\endgroup\$
1

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) 
\$\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\$
    – canni
    CommentedApr 12, 2014 at 11:52

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.