Specific-Dispatch

优美胜于丑陋
import this

前言

表驱动法是一种编辑模式(Scheme)——从表里面查找信息而不使用逻辑语句(ifcase)。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。

对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。

Python的switch case

由于Python中没有switch case关键词,所以对于每一种情况的逻辑语句只能用if,elif,else来实现。

1
2
3
4
5
6
7
def handle_case(case):
if case == 1:
print('case 1')
elif case == 2:
print('case 2')
else:
print('default case')

而当条件变得很多和各个条件的内部处理逻辑背景复杂时,整个代码块就会变得很臃肿。本文将讨论两种方案,来处理这种情况。

显式Dict实现

使用dict来索引各个case的处理函数,并在主入口统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

def handle_case(case):
case_func = {
1: handle_case_1,
2: handle_case_2
}
case_func.get(case, handle_case_default)(case)

def handle_case_1(case):
print('case 1')

def handle_case_2(case):
print('case 2')

def handle_case_default(case):
print('default case')

装饰器实现

而受到PEP-443: Single-dispatch generic functions的启发,很容易就能实现如下装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from functools import update_wrapper
from types import MappingProxyType
from typing import Hashable, Callable, Union


def specificdispatch(key: Union[int, str] = 0) -> Callable:
"""specific-dispatch generic function decorator.

Transforms a function into a generic function, which can have different
behaviours depending upon the value of its key of arguments or key of keyword arguments.
The decorated function acts as the default implementation, and additional
implementations can be registered using the register() attribute of the
generic function.
"""

def decorate(func: Callable) -> Callable:
registry = {}

def dispatch(key: Hashable) -> Callable:
"""
Runs the dispatch algorithm to return the best available implementation
for the given *key* registered on *generic_func*.
"""
try:
impl = registry[key]
except KeyError:
impl = registry[object]
return impl

def register(key: Hashable, func: Callable=None) -> Callable:
"""
Registers a new implementation for the given *key* on a *generic_func*.
"""
if func is None:
return lambda f: register(key, f)

registry[key] = func
return func

def wrapper_index(*args, **kw):
return dispatch(args[key])(*args, **kw)

def wrapper_keyword(*args, **kw):
return dispatch(kw[key])(*args, **kw)

registry[object] = func
if isinstance(key, int):
wrapper = wrapper_index
elif isinstance(key, str):
wrapper = wrapper_keyword
else:
raise KeyError('The key must be int or str')
wrapper.register = register
wrapper.dispatch = dispatch
wrapper.registry = MappingProxyType(registry)
update_wrapper(wrapper, func)

return wrapper

return decorate

而之前的代码就能很简洁的重构成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@specificdispatch(key=0)
def handle_case(case):
print('default case')

@handle_case.register(1)
def _(case):
print('case 1')

@handle_case.register(2)
def _(case):
print('case 2')

handle_case(1) # case 1
handle_case(0) # default case

而对于这样的架构,即易于扩展也利于维护。

更多实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Test:
@specificdispatch(key=1)
def test_dispatch(self, message, *args, **kw):
print(f'default: {message} args:{args} kw:{kw}')

@test_dispatch.register('test')
def _(self, message, *args, **kw):
print(f'test: {message} args:{args} kw:{kw}')

test = Test()
# default: default args:(1,) kw:{'test': True}
test.test_dispatch('default', 1, test=True)
# test: test args:(1,) kw:{'test': True}
test.test_dispatch('test', 1, test=True)

@specificdispatch(key='case')
def handle_case(case):
print('default case')

@handle_case.register(1)
def _(case):
print('case 1')

@handle_case.register(2)
def _(case):
print('case 2')

handle_case(case=1) # case 1
handle_case(case=0) # default case

总结

对比两种处理方案,区别在于显式dict的存在。对于显式的dict存在,方便索引和查看具体case和对应的处理函数,而对于case的增加或者删除,都得增加或删除对应主入口中case和func的键值对。而装饰器的存在简化了上述步骤,而对应的代价则是将dict的存在隐式化了,类似的设计模式同Web框架中路由注册。

  1. specificdispatch只是一个单纯的functool,import了就能用的那种,从行数上来说,使用装饰器和字典来说基本是没有差别的。
  2. 从性能角度来说,查表的方法(字典和装饰器)的性能都是是比 if elif 要高的,是O(1)的性能。
  3. 字典和装饰器的方法,唯一的区别也是在字典是否显式存在,以及是否需要手动维护。
0%