#!/usr/bin/env python
#
# Copyright (c) 2011 Jan Kaliszewski (zuo). All rights reserved.
# Licensed under the MIT License.
#
# Python 2.5+/3.x-compatibile.
#
# The newest version of this module should be downloadable from:
# https://github.com/zuo/Zuo-s-Recipes-and-Drafts/blob/master/caseswitch.py
from collections import defaultdict
import inspect
try: xrange
except NameError:
xrange = range # Py3.x
__all__ = 'with_switch', 'case', 'list_switch_factory', 'SwitchMeta', 'Switch'
#
# non-public stuff
class _DefaultCaseKey(object):
def __repr__(self):
return '<default>'
_DEF_CASE_KEY = _DefaultCaseKey()
# public classes and functions
def with_switch(cls):
"""
Class decorator that adds two attributes to the `cls` class:
* `switch` -- a defaultdict (or collection of other type, created with
cls.custom_switch_factory) mapping case keys to case objects (that
have been decorated with the @case() decorator;
* `get_default_case` -- a static method that returns the default case
object, i.e. one which has been decorated with @case(default=True)
(or returns None if no object has been decorated in that way).
Typically, case object is a callable method (but doesn't need to be).
"""
keys_to_cases = {}
for name, obj in inspect.getmembers(cls):
for key in getattr(obj, '_switch_case_keys', ()):
_obj = keys_to_cases.setdefault(key, obj)
if _obj is not obj:
raise ValueError('More than one case for key %r' % key)
default_case = keys_to_cases.pop(_DEF_CASE_KEY, None)
get_default_case = (lambda: default_case)
switch_factory = getattr(cls, 'custom_switch_factory', defaultdict)
kwargs = getattr(cls, 'custom_switch_factory_kwargs', {})
cls.switch = switch_factory(get_default_case, keys_to_cases, **kwargs)
cls.get_default_case = staticmethod(get_default_case)
return cls
def case(*keys, **kwargs):
"""Decorator: tags an attribute (probably a method) as a case object."""
keys = list(keys)
_default = kwargs.pop('default', False)
_itsname = kwargs.pop('itsname', False)
_classmethod = kwargs.pop('classmethod', False)
_staticmethod = kwargs.pop('staticmethod', True) # default option
if kwargs:
raise TypeError(
'case() got unexpected keyword arguments: %s' %
', '.join(sorted(kwargs)))
def case_decorator(obj):
if _default:
keys.append(_DEF_CASE_KEY)
if _itsname:
keys.append(obj.__name__)
obj._switch_case_keys = keys
if _classmethod:
return classmethod(obj)
elif _staticmethod:
return staticmethod(obj)
else:
return obj
return case_decorator
@staticmethod
def list_switch_factory(get_default_case, keys_to_cases, length=1000):
"""
Factory to create fast integer-only-based switches (using list-indexing).
A usefule example of the optional 'custom_switch_factory' attribute value.
"""
default_case = get_default_case()
switch = [keys_to_cases.pop(key, default_case) for key in xrange(length)]
if keys_to_cases:
raise ValueError(
'declared list length is too small for keys: ' +
', '.join(map(repr, sorted(keys_to_cases))))
return switch
#
# convenience classes (any of them can be used *optionally*
# -- instead of using @with_switch directly)
class SwitchMeta(type):
"""Metaclass: decorates classes created by it with @with_switch."""
def __new__(mcs, name, bases, attr_dict):
return with_switch(type.__new__(mcs, name, bases, attr_dict))
# (here: Py2.x/3.x-compatibile way to create a class with a custom metaclass)
Switch = SwitchMeta(
'Switch', (object,), {'__doc__':
"""SwitchMeta()-created class: decorates subclasses with @with_switch."""})
#
# some performance tests (with example switch declarations)...
if __name__ == '__main__':
import random
import sys
import timeit
# useful in most situations when the number of cases is not-so-small
# (for small numbers the traditional if/elif/else approach seems to be
# most efficient)
class DefaultDictSwitch(Switch):
@case(1)
def one(arg):
return 'one' + arg
@case(2)
def two(arg):
return 'two' + arg
@case(3)
def three(arg):
return 'three' + arg
@case(4)
def four(arg):
return 'four' + arg
@case(5)
def five(arg):
return 'five' + arg
@case(6)
def six(arg):
return 'six' + arg
@case(7)
def seven(arg):
return 'seven' + arg
@case(8)
def eight(arg):
return 'eight' + arg
@case(9)
def nine(arg):
return 'nine' + arg
@case(10)
def ten(arg):
return 'ten' + arg
@case(11)
def eleven(arg):
return 'eleven' + arg
@case(12)
def twelve(arg):
return 'twelve' + arg
@case(13)
def thirteen(arg):
return 'thirteen' + arg
@case(14)
def fourteen(arg):
return 'fourteen' + arg
@case(15)
def fifteen(arg):
return 'fifteen' + arg
@case(16)
def sixteen(arg):
return 'sixteen' + arg
@case(17, 71, 77)
def seventeen_plus(arg):
return 'seventeen_plus' + arg
@case(18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888)
def eighteen_plus(arg):
return 'eighteen_plus' + arg
@case(
19, 91, 99, 119, 191, 199, 911, 919, 991, 999,
1111, 1119, 1191, 1199, 1911, 1919, 1991, 1999,
9111, 9119, 9191, 9199, 9911, 9919, 9991, 9999)
def nineteen_plus(arg):
return 'nineteen_plus' + arg
@case(33, 333, 3333, default=True)
def something_else(arg):
return 'the default case' + arg
# we add some cases, basing on an already defined switch class
# (note that subclassing does not slow down the switch at all!)
class AdminCommandSwitch(DefaultDictSwitch):
@case(itsname=True) # @case('shutdown') expressed in a DRY way
def shutdown():
return 'shutdown'
@case('get-class', classmethod=True)
def get_class(cls):
return cls
@case(*xrange(30000, 40000))
def many_keys():
return 'many_keys'
# special-case optimization (for integer-only keys from a limited range)
class ListBasedSwitch(DefaultDictSwitch):
custom_switch_factory = list_switch_factory
custom_switch_factory_kwargs = {'length': 10000}
# no real advantages over DefaultDictSwitch (added here only to show that)
class DictBasedSwitch(DefaultDictSwitch):
custom_switch_factory = staticmethod(
lambda get_default_case, keys_to_cases: dict(keys_to_cases))
def test_standard():
"standard switch (out-of-the-box default case support)"
switch = DefaultDictSwitch.switch
x = ''
for key in case_keys:
x = switch[key](x[:10])
def test_standard2():
"another standard switch -- subclass with more cases"
switch = AdminCommandSwitch.switch
x = ''
for key in case_keys:
x = switch[key](x[:10])
assert switch['shutdown']() == 'shutdown'
assert switch['get-class']() is AdminCommandSwitch
assert switch[34567]() == 'many_keys'
def test_list_based_range_default():
"list-based switch (default case support for keys from the range)"
switch = ListBasedSwitch.switch
x = ''
for key in case_keys:
x = switch[key](x[:10])
def test_list_based_with_try_except():
"list-based switch + additional try/except-based default case support"
switch = ListBasedSwitch.switch
default_case = ListBasedSwitch.get_default_case()
_error = IndexError
x = ''
for key in case_keys:
try:
x = switch[key](x[:10])
except _error:
x = default_case(x[:10])
def test_dict_based_no_default():
"ordinary-dict-based switch, no default case support"
switch = DictBasedSwitch.switch
x = ''
for key in case_keys:
x = switch[key](x[:10])
def test_dict_based_with_get():
"ordinary-dict-based switch + dict.get()-based default case support"
switch = DictBasedSwitch.switch
default_case = DictBasedSwitch.get_default_case()
x = ''
for key in case_keys:
x = switch.get(key, default_case)(x[:10])
def test_dict_based_with_try_except():
"ordinary-dict-based switch + try/except-based default case support"
switch = DictBasedSwitch.switch
default_case = DictBasedSwitch.get_default_case()
_error = KeyError
x = ''
for key in case_keys:
try:
x = switch[key](x[:10])
except _error:
x = default_case(x[:10])
def test_if_elif():
"traditional if/elif.../else sequence"
x = ''
for key in case_keys:
if key == 1:
x = 'one' + x[:10]
elif key == 2:
x = 'two' + x[:10]
elif key == 3:
x = 'three' + x[:10]
elif key == 4:
x = 'four' + x[:10]
elif key == 5:
x = 'five' + x[:10]
elif key == 6:
x = 'six' + x[:10]
elif key == 7:
x = 'seven' + x[:10]
elif key == 8:
x = 'eight' + x[:10]
elif key == 9:
x = 'nine' + x[:10]
elif key == 10:
x = 'ten' + x[:10]
elif key == 11:
x = 'eleven' + x[:10]
elif key == 12:
x = 'twelve' + x[:10]
elif key == 13:
x = 'thirteen' + x[:10]
elif key == 14:
x = 'fourteen' + x[:10]
elif key == 15:
x = 'fifteen' + x[:10]
elif key == 16:
x = 'sixteen' + x[:10]
elif key in (17, 71, 77):
x = 'seventeen_plus' + x[:10]
elif key in (18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888):
x = 'eighteen_plus' + x[:10]
elif key in (
19, 91, 99, 111, 119, 191, 199, 911, 919, 991, 999,
1111, 1119, 1191, 1199, 1911, 1919, 1991, 1999):
x = 'nineteen_plus' + x[:10]
else:
x = 'the default case' + x[:10]
def test_tour(test_seq, msg, case_keys_choice, case_keys_length=1000000):
global case_keys
case_keys_choice = sorted(case_keys_choice)
print(
'\ngenerating random-ordered, %d-item-long, '
'sequence of keys from the set: {%s}...' %
(case_keys_length, ', '.join(map(repr, case_keys_choice))))
case_keys = [
random.choice(case_keys_choice) for i in xrange(case_keys_length)]
print('\n' + msg)
fastest_tests = []
for test in test_seq:
try:
results = timeit.Timer(
'test()',
'from __main__ import %s as test' % test.__name__,
).repeat(number=1, repeat=3)
except Exception:
print('* %s: [could not be used]' % test.__doc__)
else:
fastest = min(results)
print('* %s: %s (fastest: %f)' % (
test.__doc__,
', '.join('%f' % t for t in results),
fastest))
fastest_tests.append((fastest, test.__doc__))
if fastest_tests:
print('\nthe winner is: %f/%s' % (min(fastest_tests)))
print('%r simple performance tests' % sys.argv[0])
test_seq = (
test_if_elif,
test_standard,
test_standard2,
test_list_based_range_default,
test_list_based_with_try_except,
test_dict_based_no_default,
test_dict_based_with_get,
test_dict_based_with_try_except,
)
test_tour(
test_seq,
'test tour #1 (default cases not used; small number of keys):',
case_keys_choice = tuple(xrange(1, 10)))
test_tour(
test_seq,
'test tour #2 (default cases not used; not-so-small number of keys):',
case_keys_choice = (tuple(xrange(1, 20)) + (
71, 77, 18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888,
91, 99, 119, 191, 199, 911, 919, 991, 999,
1111, 1119, 1191, 1199, 1911, 1919, 1991, 1999,
9111, 9119, 9191, 9199, 9911, 9919, 9991, 9999)))
test_tour(
test_seq,
'test tour #3 (using default cases, keys in list-based switch range):',
# about half of the keys do not have their cases (default case is used)
case_keys_choice = (tuple(xrange(1, 70)) + (
71, 77, 18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888,
91, 99, 119, 191, 199, 911, 919, 991, 999,
1111, 1119, 1191, 1199, 1911, 1919, 1991, 1999,
9111, 9119, 9191, 9199, 9911, 9919, 9991, 9999)))
test_tour(
test_seq,
'test tour #4 (using default cases, their keys not in that range):',
# about half of the keys do not have their cases (default case is used)
# + that keys are not in the list-based switch key range
case_keys_choice=(tuple(xrange(1, 20)) + (
71, 77, 18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888,
91, 99, 119, 191, 199, 911, 919, 991, 999,
1111, 1119, 1191, 1199, 1911, 1919, 1991, 1999,
9111, 9119, 9191, 9199, 9911, 9919, 9991, 9999) +
tuple(xrange(10000, 10050))))
Diff to Previous Revision
--- revision 3 2011-09-02 01:34:56
+++ revision 4 2011-09-02 01:49:40
@@ -126,9 +126,9 @@
import timeit
- # universal approach -- useful in most situations when the number of cases
- # is not very small (for a-few-cases situations the traditional if/elif...
- # approach seems to be most efficient)
+ # useful in most situations when the number of cases is not-so-small
+ # (for small numbers the traditional if/elif/else approach seems to be
+ # most efficient)
class DefaultDictSwitch(Switch):
@case(1)
@@ -399,7 +399,12 @@
test_tour(
test_seq,
- '1st test tour (default cases not used):', # all keys have their cases
+ 'test tour #1 (default cases not used; small number of keys):',
+ case_keys_choice = tuple(xrange(1, 10)))
+
+ test_tour(
+ test_seq,
+ 'test tour #2 (default cases not used; not-so-small number of keys):',
case_keys_choice = (tuple(xrange(1, 20)) + (
71, 77, 18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888,
91, 99, 119, 191, 199, 911, 919, 991, 999,
@@ -408,7 +413,7 @@
test_tour(
test_seq,
- 'test tour #2 (using default cases, keys in list-based switch range):',
+ 'test tour #3 (using default cases, keys in list-based switch range):',
# about half of the keys do not have their cases (default case is used)
case_keys_choice = (tuple(xrange(1, 70)) + (
71, 77, 18, 81, 88, 111, 118, 181, 188, 811, 818, 881, 888,
@@ -418,7 +423,7 @@
test_tour(
test_seq,
- 'test tour #3 (using default cases, their keys not in that range):',
+ 'test tour #4 (using default cases, their keys not in that range):',
# about half of the keys do not have their cases (default case is used)
# + that keys are not in the list-based switch key range
case_keys_choice=(tuple(xrange(1, 20)) + (