优美胜于丑陋 import this
博客地址:Specific-Dispatch
表驱动法是一种编辑模式( Scheme )——从表里面查找信息而不使用逻辑语句(if 和 case)。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。
对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。
由于 Python 中没有switch case关键词,所以对于每一种情况的逻辑语句只能用if
,elif
,else
来实现,显得很不 Pythonic.
def handle_case(case):
if case == 1:
print('case 1')
elif case == 2:
print('case 2')
else:
print('default case')
而受到PEP-443: Single-dispatch generic functions的启发,很容易就能实现如下装饰器:
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
而之前的代码就能很优美的重构成这样:
@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
而对于这样的架构,即易于扩展也利于维护。
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
1
luguhu 2018-10-14 08:54:05 +08:00 via Android 2
用字典不好吗?
|
2
luguhu 2018-10-14 08:56:27 +08:00 via Android
emmm,没别的意思。只是想知道用字典有什么不好的。
|
3
keysona 2018-10-14 09:09:10 +08:00
其实,我个人觉得,字典更简单,也更容易维护。
你这个好像复杂化了。 |
4
ltoddy 2018-10-14 09:10:09 +08:00
@luguhu 说的很对, 向 if-else 多了,本身就会降低代码质量, 毕竟这是硬编码. 通过 dict 转化成软编码,提高程序的可扩展性.
|
5
GreatTony OP @keysona 这个本质就是字典呀,只是相当于把字典封装起来了,然后不用单独去维护字典,在需要使用扩展新的 case 时,使用注册机制而已。
|
6
virusdefender 2018-10-14 09:36:12 +08:00
每一种情况的逻辑语句只能用 if,elif,else 来实现,显得很不 Pythonic
---- 没觉得这样不 Pythonic |
7
codechaser 2018-10-14 09:38:46 +08:00
switch 语句有 default 输出,而这样用装饰器如果传入的 key 不是 0,1,或 2,而是 3,不就会引发 keyError 吗?
|
8
GreatTony OP @virusdefender 我在前言里也说了,条件很少的时候,以及每个条件对应的逻辑不复杂的时候 If else 的很简单明了的。
但一旦条件很多,而且内部逻辑比较多的情况下,使用查表的方式会显得清晰明了。 其次,我是根据 PEP-443 做了一个扩展而言,PEP 不 Pythonic 吗? |
9
monkeylyf 2018-10-14 09:43:39 +08:00
个人认为,if.else 作为最基本的逻辑控制,和 pythonic 没什么关系。
如果 if branch 里面的逻辑复杂,显得整个 if else 代码块在“视觉”上不优美,可以把逻辑封装到 function 里。 同楼上讲的,用一个 dict<case, function>, 基本可以保证代码的可读性。 把别的语言的特性搬进 python 本身就显得不是很 pythonic。个人愚见。 |
10
GreatTony OP @codechaser 额,你没看例子吗?第一个装饰器就是默认情况,之后的 register 才是其他 case。
|
11
di94sh 2018-10-14 09:50:14 +08:00 via Android
虽然不如用字典映射方便,但是还是学习了一种新思路,感谢。
|
12
tumbzzc 2018-10-14 09:50:17 +08:00 via Android
感觉复杂化了
|
13
GreatTony OP @monkeylyf 直接自己维护 dict 的话,会有多余步骤:
1: 编写对应的 case 处理函数 handle_case_new 2: 将 handle_case_new 函数加到主 handle_case 函数中的 dict 中 我使用装饰器,也就是把这两部合在一个区域而言,对于维护者和扩展而言,是更为方便的。而装饰器是 Python 中非常实用且优雅的特性之一。 |
14
aaron61 2018-10-14 09:54:25 +08:00 via Android
好复杂 没看懂
|
15
monkeylyf 2018-10-14 10:21:04 +08:00
@GreatTony
1. 我可能没理解正确:对应的 case 具体处理函数不管在任何情况下都要编写,我不是很理解为什么存在多余不多余的情况 2. 函数加入 dict,从你的设计来看,确实是只需要加一个装饰器即可。如果按照我的想法封装在 dict 里面,我个人不同意这是一个多余的步骤,比如就在 dict 初始化时一步完成:func_mapping = { "case1": handle_case_1_func, "case": handle_case_2_func, ...} 追加两点: 1. 除非是把所有 case handling 函数强行封装在某个单独文件或者某个 class 里面,按照你的设计,这些函数理论上可以随意分布,即虽然你给的例子,三个函数是连续定义的,但是实际操作中可以被任何别的语块割裂。另外你的 register 是偏隐性,和 dict 的 explictly 定义,后者可读性更强。 2. 抛开维护和扩展而言,设计此类特性,更偏向于需求方的要求。decrator 在某些 use case 下是很优雅,但是不代表因为优雅就会去使用 |
16
windgo 2018-10-14 10:35:32 +08:00
<代码大全>里面有一个章节讲了 switch/if else 怎么写, 其中也说了表驱动法.
|
17
chengxiao 2018-10-14 10:40:19 +08:00 via iPhone
我到觉得字典映射加 if else 可读性更高一些……
|
18
GreatTony OP @monkeylyf 的确,在有显式的 dict 的存在时,在各个处理函数被割裂的情况下,也能很方便索引以及查看其对应的 case 的函数。
我提到的多余步骤只是说在编写完一个新的 case func 时,要返回主函数添加对应的 case 和 func 的键值对,反之亦然。简化了这一步骤自然就得显式的 dict 隐式化了,有舍有得的嘛,这就和 Web 框架中,路由注册一样的逻辑。 综上,毕竟我们这也是讨论设计模式而已,所以呢,肯定各有优缺点嘛。 |
19
laoyur 2018-10-14 12:05:40 +08:00
python 渣表示,你这个太难看懂了,说的不是装饰器的实现,而是最后的实际代码,一大坨,而且还夹杂 def 在普通的业务逻辑中,没用过的人难以理解,就算是你自己,隔两个礼拜再看也要花点时间去回忆和理解
所以在我看来,完全没有 if else 直白好用 |
20
designer 2018-10-14 12:17:14 +08:00 via iPhone
还以为你通过 python DIY 了 switch 游戏主机
|
21
cocofe0 2018-10-14 12:58:17 +08:00
我觉得用 dict 进行 case 和 func 的管理,最大的不便就是每次添加 case 都需要手动维护 dict,手动维护的都可能出现问题,而用装饰器能将维护 dict 自动化,这是最大的优点,其次,代码也更加简洁,并不觉得会特别难理解,(如果 dict 会频繁更新,我觉得这样做还是很有必要的)
|
23
littleshy 2018-10-14 13:07:39 +08:00
Simple is better than complex.
|
24
e9e499d78f 2018-10-14 13:11:45 +08:00
太 pythonic 了
|
25
zzj0311 2018-10-14 13:31:10 +08:00 via Android
为什么 Python 没有 switch case,因为没有必要~
|
26
newtype0092 2018-10-14 14:47:34 +08:00
你那句话化简一下就是:
“由于 Python 中没有。。。显得很不 Pythonic ”。 所以说 Python 的特性不 Pythonic ?好矛盾的一门语言。。。 |
27
megachweng 2018-10-14 15:59:56 +08:00
多了一种思路吧
|
28
Raisu 2018-10-14 17:14:35 +08:00 via Android
字典
|
29
neoblackcap 2018-10-14 17:28:24 +08:00
上面说了那么多,其实就是量小的时候用 if-else if-else 完全没有问题。
至于字典行不行?当然是行的啊,用字典属于表驱动模式的一种实现,完全是合乎软件工程的要求的。 |
30
lihongjie0209 2018-10-14 17:35:26 +08:00
可读性直线下降
|
31
BingoXuan 2018-10-14 18:25:00 +08:00 via Android
我们家 tinyrpc 框架就是这样实现的,管理大量函数调用时候很方便。但 team leader 就非常喜欢手动分拆多个还用字典再手动管理,简直蛋疼。
|
32
PythonAnswer 2018-10-14 19:37:54 +08:00 via iPhone
10 个以内 手写 if
10 个以外 手写字典 怎么简单怎么来啊 |
33
laqow 2018-10-14 19:47:51 +08:00 via Android
可读性和性能都下降
|
34
mseasons 2018-10-14 20:04:06 +08:00
代码量 UPUP
|
35
GreatTony OP 在这里总结一下,我博客里的内容也更新了,在文章最上面也有地址:
对比两种处理方案,区别在于显式*dict*的存在。对于显式的 dict 存在,方便索引和查看具体 case 和对应的处理函数,而对于 case 的增加或者删除,都得增加或删除对应主入口中 case 和 func 的键值对。而装饰器的存在简化了上述步骤,而对应的代价则是将 dict 的存在隐式化了,类似的设计模式同 Web 框架中路由注册。 1. specificdispatch 只是一个单纯的 functool,import 了就能用的那种,从行数上来说,使用装饰器和字典来说基本是没有差别的。 2. 从性能角度来说,查表的方法(字典和装饰器)的性能都是是比 `if` `elif` 要高的,是 O(1)的性能。 3. 字典和装饰器的方法,唯一的区别也是在字典是否显式存在,以及是否需要手动维护。 |
36
luguhu 2018-10-14 23:48:35 +08:00 via Android
嗯,明白了。这样确实更好维护,符合开放封闭原则。不过只限定 int 和 str 是不是不太好, 毕竟不只这两个可以做 key。以及 参数限定一个 是不是不太够。
|
37
caoz 2018-10-15 00:27:23 +08:00
"而装饰器的存在简化了上述步骤,而对应的代价则是将 dict 的存在隐式化了,类似的设计模式同 Web 框架中路由注册"
你是指 Flask 中的 route() 吗?个人感觉这种写法用不好很容易造成混乱,完全不如集中写在一块清晰明了,如: https://docs.djangoproject.com/en/2.1/topics/http/urls/#example https://www.tornadoweb.org/en/stable/guide/structure.html#the-application-object |
38
20015jjw 2018-10-15 00:33:46 +08:00 via Android
感觉瞎折腾
|
39
TJT 2018-10-15 01:45:13 +08:00
书读的太少, 瞎折腾, 不过思路不错, 只是不适合而已.
@GreatTony 性能角度上来说, 量少的话 if else 是比较快的. 另外 Python dict 内存效率并不高. @caoz 你想的话, 也可以写一块: http://flask.pocoo.org/docs/1.0/api/#flask.Flask.add_url_rule |
40
deepreader 2018-10-15 02:46:12 +08:00
我觉得想法不错,而且省了很多 if-else statement.
有个疑问,这个需要 case key 能 hashable,万一我的 if 的条件判断很复杂怎么办?判断条件并不是简单地 case == 1 etc. |
41
deepreader 2018-10-15 02:46:40 +08:00
@20015jjw 老板又见你了
|
42
20015jjw 2018-10-15 02:52:23 +08:00
@deepreader 羡慕大佬一波点评
|
43
zhzer 2018-10-15 04:30:00 +08:00
这也很不 Pythonic 吧...
|
44
ackfin01 2018-10-15 08:16:16 +08:00
是一种方法,怎敢叫最佳实践。。2333
|
45
araraloren 2018-10-15 08:48:59 +08:00
我也觉得 python 没有 switch case 很不 Pythonic (逃
|
46
GreatTony OP @luguhu key 这个参数是标注着 func 中需要识别参数的位置或名称的,判断条件是任何可 hash 的,注释还是都写清楚了的
|
47
GreatTony OP @TJT https://docs.python.org/3.6/whatsnew/3.6.html#new-dict-implementation Python3.5+之后已经大幅度优化了 dict 的存储模型,基本的模式以及对应的算法以及算是最优的了。然后我前言里就说了,逻辑链少的时候用 if elif 完全没问题。
|
48
GreatTony OP @deepreader singledispatch 的初衷是提供一种 Python 的函数重载机制的实现,我这个也差不多。如果你条件判断比较复杂的话,是不推荐是用隐式的判断设计的,那才是真的雪上加霜,尽管是可以实现的。
|
49
catsoul 2018-10-15 09:56:57 +08:00
我个人比较赞成 LZ 的方案,当你需要加入字典的方法分布在项目中多个不同源文件的情况,这种方式效率和错误率都大大降低。我个人的理念是:能让程序干的事情,为啥要手动。
|
51
wutiantong 2018-10-15 10:47:56 +08:00
感觉楼主的路子走歪了,这样下去眼看要走火入魔啦
|
52
troywinter 2018-10-15 15:11:16 +08:00
书读的少,歪门邪道
|
53
pythonee 2018-10-18 21:24:21 +08:00
挺 pythonic 的呀
|
54
clamshine 2018-11-13 10:31:16 +08:00
受教 多谢
|