首页   注册   登录
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 · 58 天前 · 3204 次点击
    这是一个创建于 58 天前的主题,其中的信息可能已经有所发展或是发生改变。

    优美胜于丑陋 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
        1
    luguhu   57 天前 via Android   ♥ 2
    用字典不好吗?
        2
    luguhu   57 天前 via Android
    emmm,没别的意思。只是想知道用字典有什么不好的。
        3
    keysona   57 天前
    其实,我个人觉得,字典更简单,也更容易维护。

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

    ----

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

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

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

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

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

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

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

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

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

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

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