首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
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
华为云
V2EX  ›  Python

Python Switch Case 最佳实践

  •  1
     
  •   GreatTony · 5 天前 · 2957 次点击

    优美胜于丑陋 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
    
    53 回复  |  直到 2018-10-18 21:24:21 +08:00
        1
    luguhu   5 天前 via Android   ♥ 2
    用字典不好吗?
        2
    luguhu   5 天前 via Android
    emmm,没别的意思。只是想知道用字典有什么不好的。
        3
    keysona   5 天前
    其实,我个人觉得,字典更简单,也更容易维护。

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

    ----

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

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

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

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

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

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

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

    1. specificdispatch 只是一个单纯的 functool,import 了就能用的那种,从行数上来说,使用装饰器和字典来说基本是没有差别的。
    2. 从性能角度来说,查表的方法(字典和装饰器)的性能都是是比 `if` `elif` 要高的,是 O(1)的性能。
    3. 字典和装饰器的方法,唯一的区别也是在字典是否显式存在,以及是否需要手动维护。
        36
    luguhu   5 天前 via Android
    嗯,明白了。这样确实更好维护,符合开放封闭原则。不过只限定 int 和 str 是不是不太好, 毕竟不只这两个可以做 key。以及 参数限定一个 是不是不太够。
        37
    caoz   5 天前
    "而装饰器的存在简化了上述步骤,而对应的代价则是将 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   5 天前 via Android
    感觉瞎折腾
        39
    TJT   5 天前
    书读的太少, 瞎折腾, 不过思路不错, 只是不适合而已.

    @GreatTony 性能角度上来说, 量少的话 if else 是比较快的. 另外 Python dict 内存效率并不高.

    @caoz 你想的话, 也可以写一块: http://flask.pocoo.org/docs/1.0/api/#flask.Flask.add_url_rule
        40
    deepreader   5 天前
    我觉得想法不错,而且省了很多 if-else statement.

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