V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
GreatTony
V2EX  ›  Python

Python Switch Case 最佳实践

  •  1
     
  •   GreatTony ·
    GuangTianLi · 2018-10-14 08:32:47 +08:00 · 6316 次点击
    这是一个创建于 1993 天前的主题,其中的信息可能已经有所发展或是发生改变。

    优美胜于丑陋 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
    
    54 条回复    2018-11-13 10:31:16 +08:00
    luguhu
        1
    luguhu  
       2018-10-14 08:54:05 +08:00 via Android   ❤️ 2
    用字典不好吗?
    luguhu
        2
    luguhu  
       2018-10-14 08:56:27 +08:00 via Android
    emmm,没别的意思。只是想知道用字典有什么不好的。
    keysona
        3
    keysona  
       2018-10-14 09:09:10 +08:00
    其实,我个人觉得,字典更简单,也更容易维护。

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

    ----

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

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

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

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

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

    综上,毕竟我们这也是讨论设计模式而已,所以呢,肯定各有优缺点嘛。
    laoyur
        19
    laoyur  
       2018-10-14 12:05:40 +08:00
    python 渣表示,你这个太难看懂了,说的不是装饰器的实现,而是最后的实际代码,一大坨,而且还夹杂 def 在普通的业务逻辑中,没用过的人难以理解,就算是你自己,隔两个礼拜再看也要花点时间去回忆和理解
    所以在我看来,完全没有 if else 直白好用
    designer
        20
    designer  
       2018-10-14 12:17:14 +08:00 via iPhone
    还以为你通过 python DIY 了 switch 游戏主机
    cocofe0
        21
    cocofe0  
       2018-10-14 12:58:17 +08:00
    我觉得用 dict 进行 case 和 func 的管理,最大的不便就是每次添加 case 都需要手动维护 dict,手动维护的都可能出现问题,而用装饰器能将维护 dict 自动化,这是最大的优点,其次,代码也更加简洁,并不觉得会特别难理解,(如果 dict 会频繁更新,我觉得这样做还是很有必要的)
    bucky
        22
    bucky  
       2018-10-14 13:03:26 +08:00
    @cocofe0 那直接把字典包装一下不就行了
    littleshy
        23
    littleshy  
       2018-10-14 13:07:39 +08:00
    Simple is better than complex.
    e9e499d78f
        24
    e9e499d78f  
       2018-10-14 13:11:45 +08:00
    太 pythonic 了
    zzj0311
        25
    zzj0311  
       2018-10-14 13:31:10 +08:00 via Android
    为什么 Python 没有 switch case,因为没有必要~
    newtype0092
        26
    newtype0092  
       2018-10-14 14:47:34 +08:00
    你那句话化简一下就是:
    “由于 Python 中没有。。。显得很不 Pythonic ”。
    所以说 Python 的特性不 Pythonic ?好矛盾的一门语言。。。
    megachweng
        27
    megachweng  
       2018-10-14 15:59:56 +08:00
    多了一种思路吧
    Raisu
        28
    Raisu  
       2018-10-14 17:14:35 +08:00 via Android
    字典
    neoblackcap
        29
    neoblackcap  
       2018-10-14 17:28:24 +08:00
    上面说了那么多,其实就是量小的时候用 if-else if-else 完全没有问题。
    至于字典行不行?当然是行的啊,用字典属于表驱动模式的一种实现,完全是合乎软件工程的要求的。
    lihongjie0209
        30
    lihongjie0209  
       2018-10-14 17:35:26 +08:00
    可读性直线下降
    BingoXuan
        31
    BingoXuan  
       2018-10-14 18:25:00 +08:00 via Android
    我们家 tinyrpc 框架就是这样实现的,管理大量函数调用时候很方便。但 team leader 就非常喜欢手动分拆多个还用字典再手动管理,简直蛋疼。
    PythonAnswer
        32
    PythonAnswer  
       2018-10-14 19:37:54 +08:00 via iPhone
    10 个以内 手写 if
    10 个以外 手写字典
    怎么简单怎么来啊
    laqow
        33
    laqow  
       2018-10-14 19:47:51 +08:00 via Android
    可读性和性能都下降
    mseasons
        34
    mseasons  
       2018-10-14 20:04:06 +08:00
    代码量 UPUP
    GreatTony
        35
    GreatTony  
    OP
       2018-10-14 21:01:15 +08:00
    在这里总结一下,我博客里的内容也更新了,在文章最上面也有地址:

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

    1. specificdispatch 只是一个单纯的 functool,import 了就能用的那种,从行数上来说,使用装饰器和字典来说基本是没有差别的。
    2. 从性能角度来说,查表的方法(字典和装饰器)的性能都是是比 `if` `elif` 要高的,是 O(1)的性能。
    3. 字典和装饰器的方法,唯一的区别也是在字典是否显式存在,以及是否需要手动维护。
    luguhu
        36
    luguhu  
       2018-10-14 23:48:35 +08:00 via Android
    嗯,明白了。这样确实更好维护,符合开放封闭原则。不过只限定 int 和 str 是不是不太好, 毕竟不只这两个可以做 key。以及 参数限定一个 是不是不太够。
    caoz
        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
    20015jjw
        38
    20015jjw  
       2018-10-15 00:33:46 +08:00 via Android
    感觉瞎折腾
    TJT
        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
    deepreader
        40
    deepreader  
       2018-10-15 02:46:12 +08:00
    我觉得想法不错,而且省了很多 if-else statement.

    有个疑问,这个需要 case key 能 hashable,万一我的 if 的条件判断很复杂怎么办?判断条件并不是简单地 case == 1 etc.
    deepreader
        41
    deepreader  
       2018-10-15 02:46:40 +08:00
    @20015jjw 老板又见你了
    20015jjw
        42
    20015jjw  
       2018-10-15 02:52:23 +08:00
    @deepreader 羡慕大佬一波点评
    zhzer
        43
    zhzer  
       2018-10-15 04:30:00 +08:00
    这也很不 Pythonic 吧...
    ackfin01
        44
    ackfin01  
       2018-10-15 08:16:16 +08:00
    是一种方法,怎敢叫最佳实践。。2333
    araraloren
        45
    araraloren  
       2018-10-15 08:48:59 +08:00
    我也觉得 python 没有 switch case 很不 Pythonic (逃
    GreatTony
        46
    GreatTony  
    OP
       2018-10-15 09:14:26 +08:00
    @luguhu key 这个参数是标注着 func 中需要识别参数的位置或名称的,判断条件是任何可 hash 的,注释还是都写清楚了的
    GreatTony
        47
    GreatTony  
    OP
       2018-10-15 09:17:19 +08:00
    @TJT https://docs.python.org/3.6/whatsnew/3.6.html#new-dict-implementation Python3.5+之后已经大幅度优化了 dict 的存储模型,基本的模式以及对应的算法以及算是最优的了。然后我前言里就说了,逻辑链少的时候用 if elif 完全没问题。
    GreatTony
        48
    GreatTony  
    OP
       2018-10-15 09:25:31 +08:00   ❤️ 1
    @deepreader singledispatch 的初衷是提供一种 Python 的函数重载机制的实现,我这个也差不多。如果你条件判断比较复杂的话,是不推荐是用隐式的判断设计的,那才是真的雪上加霜,尽管是可以实现的。
    catsoul
        49
    catsoul  
       2018-10-15 09:56:57 +08:00
    我个人比较赞成 LZ 的方案,当你需要加入字典的方法分布在项目中多个不同源文件的情况,这种方式效率和错误率都大大降低。我个人的理念是:能让程序干的事情,为啥要手动。
    catsoul
        50
    catsoul  
       2018-10-15 09:58:51 +08:00
    @catsoul 效率提高,少打了俩字儿
    wutiantong
        51
    wutiantong  
       2018-10-15 10:47:56 +08:00
    感觉楼主的路子走歪了,这样下去眼看要走火入魔啦
    troywinter
        52
    troywinter  
       2018-10-15 15:11:16 +08:00
    书读的少,歪门邪道
    pythonee
        53
    pythonee  
       2018-10-18 21:24:21 +08:00
    挺 pythonic 的呀
    clamshine
        54
    clamshine  
       2018-11-13 10:31:16 +08:00
    受教 多谢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5402 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 06:47 · PVG 14:47 · LAX 23:47 · JFK 02:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.