Python Switch Case 最佳实践

2018-10-14 08:32:47 +08:00
 GreatTony

优美胜于丑陋 import this

博客地址:Specific-Dispatch

前言

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

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

Python 的switch 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
6369 次点击
所在节点    Python
54 条回复
luguhu
2018-10-14 08:54:05 +08:00
用字典不好吗?
luguhu
2018-10-14 08:56:27 +08:00
emmm,没别的意思。只是想知道用字典有什么不好的。
keysona
2018-10-14 09:09:10 +08:00
其实,我个人觉得,字典更简单,也更容易维护。

你这个好像复杂化了。
ltoddy
2018-10-14 09:10:09 +08:00
@luguhu 说的很对, 向 if-else 多了,本身就会降低代码质量, 毕竟这是硬编码. 通过 dict 转化成软编码,提高程序的可扩展性.
GreatTony
2018-10-14 09:29:32 +08:00
@keysona 这个本质就是字典呀,只是相当于把字典封装起来了,然后不用单独去维护字典,在需要使用扩展新的 case 时,使用注册机制而已。
virusdefender
2018-10-14 09:36:12 +08:00
每一种情况的逻辑语句只能用 if,elif,else 来实现,显得很不 Pythonic

----

没觉得这样不 Pythonic
codechaser
2018-10-14 09:38:46 +08:00
switch 语句有 default 输出,而这样用装饰器如果传入的 key 不是 0,1,或 2,而是 3,不就会引发 keyError 吗?
GreatTony
2018-10-14 09:43:24 +08:00
@virusdefender 我在前言里也说了,条件很少的时候,以及每个条件对应的逻辑不复杂的时候 If else 的很简单明了的。
但一旦条件很多,而且内部逻辑比较多的情况下,使用查表的方式会显得清晰明了。

其次,我是根据 PEP-443 做了一个扩展而言,PEP 不 Pythonic 吗?
monkeylyf
2018-10-14 09:43:39 +08:00
个人认为,if.else 作为最基本的逻辑控制,和 pythonic 没什么关系。
如果 if branch 里面的逻辑复杂,显得整个 if else 代码块在“视觉”上不优美,可以把逻辑封装到 function 里。
同楼上讲的,用一个 dict<case, function>, 基本可以保证代码的可读性。
把别的语言的特性搬进 python 本身就显得不是很 pythonic。个人愚见。
GreatTony
2018-10-14 09:44:22 +08:00
@codechaser 额,你没看例子吗?第一个装饰器就是默认情况,之后的 register 才是其他 case。
di94sh
2018-10-14 09:50:14 +08:00
虽然不如用字典映射方便,但是还是学习了一种新思路,感谢。
tumbzzc
2018-10-14 09:50:17 +08:00
感觉复杂化了
GreatTony
2018-10-14 09:51:04 +08:00
@monkeylyf 直接自己维护 dict 的话,会有多余步骤:

1: 编写对应的 case 处理函数 handle_case_new
2: 将 handle_case_new 函数加到主 handle_case 函数中的 dict 中

我使用装饰器,也就是把这两部合在一个区域而言,对于维护者和扩展而言,是更为方便的。而装饰器是 Python 中非常实用且优雅的特性之一。
aaron61
2018-10-14 09:54:25 +08:00
好复杂 没看懂
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 下是很优雅,但是不代表因为优雅就会去使用
windgo
2018-10-14 10:35:32 +08:00
<代码大全>里面有一个章节讲了 switch/if else 怎么写, 其中也说了表驱动法.
chengxiao
2018-10-14 10:40:19 +08:00
我到觉得字典映射加 if else 可读性更高一些……
GreatTony
2018-10-14 11:11:15 +08:00
@monkeylyf 的确,在有显式的 dict 的存在时,在各个处理函数被割裂的情况下,也能很方便索引以及查看其对应的 case 的函数。

我提到的多余步骤只是说在编写完一个新的 case func 时,要返回主函数添加对应的 case 和 func 的键值对,反之亦然。简化了这一步骤自然就得显式的 dict 隐式化了,有舍有得的嘛,这就和 Web 框架中,路由注册一样的逻辑。

综上,毕竟我们这也是讨论设计模式而已,所以呢,肯定各有优缺点嘛。
laoyur
2018-10-14 12:05:40 +08:00
python 渣表示,你这个太难看懂了,说的不是装饰器的实现,而是最后的实际代码,一大坨,而且还夹杂 def 在普通的业务逻辑中,没用过的人难以理解,就算是你自己,隔两个礼拜再看也要花点时间去回忆和理解
所以在我看来,完全没有 if else 直白好用
designer
2018-10-14 12:17:14 +08:00
还以为你通过 python DIY 了 switch 游戏主机

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/497441

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX