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
    Apr 12, 2014 at 11:52

Your Answer

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

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