ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python functools.singledispatch 实战:用单分派消灭一堆 isinstance 判断

Python functools.singledispatch 实战:用单分派消灭一堆 isinstance 判断 Python functools.singledispatch 实战:用单分派消灭一堆 isinstance 判断写 Python 时你大概率见过这种函数:开头一长串if isinstance(x, int): ... elif isinstance(x, str): ... elif isinstance(x, list): ...。每加一种类型就往里塞一个分支,函数越来越臃肿,改一处怕碰坏别处。标准库的functools.singledispatch就是专门治这个的——它让你按参数类型把逻辑拆成独立的函数,新增类型时不用动老代码。这篇手把手带你用起来。先看看 isinstance 分支有多难维护假设要写一个把不同类型序列化成「适合展示的字符串」的函数:defto_display(value)-str:ifisinstance(value,bool):# 注意 bool 必须放在 int 前面return是ifvalueelse否elifisinstance(value,int):returnf{value:,}# 千分位elifisinstance(value,float):returnf{value:.2f}elifisinstance(value,list):return, .join(to_display(v)forvinvalue)else:returnstr(value)问题很明显:所有类型的逻辑挤在一个函数里;分支顺序还暗藏陷阱(bool是int的子类,放后面永远命中不了);想让第三方新增一种类型的处理,只能改这个函数的源码。用 singledispatch 拆开singledispatch把「根据第一个参数的类型分发」这件事交给标准库。先定义一个泛型函数作为默认实现,再用.register为具体类型注册各自的实现:fromfunctoolsimportsingledispatchsingledispatchdefto_display(value)-str:# 默认实现:兜底所有没单独注册的类型returnstr(value)to_display.registerdef_(value:int)-str:returnf{value:,}to_display.registerdef_(value:float)-str:returnf{value:.2f}to_display.registerdef_(value:list)-str:return, .join(to_display(v)forvinvalue)注意几个细节:被singledispatch装饰的是默认/兜底实现;每个register的函数名都可以写成_,因为我们从不直接调用它们,只通过to_display(x)触发分派。类型直接写在参数注解里(Python 3.7 支持这种写法),读起来很清爽。调用方式和普通函数完全一样:print(to_display(1234567))# 1,234,567print(to_display(3.14159))# 3.14print(to_display([1,2.5,hi]))# 1, 2.50, hiprint(to_display({a:1}))# {a: 1}(走默认实现)子类会自动匹配最近的父类singledispatch按 MRO(方法解析顺序)查找,注册父类后,子类实例会自动命中父类的实现。这对处理一族类型特别省事:fromcollections.abcimportSequenceto_display.registerdef_(value:Sequence)-str:return | .join(str(v)forvinvalue)# tuple 是 Sequence 的子类,自动走上面这个实现print(to_display((x,y)))# x | y如果同时注册了list和它的父类Sequence,list实例会优先命中更具体的list实现——「最具体的优先」,这正是我们想要的。bool 陷阱:它比 int 更具体前面提到bool是int的子类。用 isinstance 分支时得手动把 bool 判断放前面,而 singledispatch 天然「就近匹配」,你只要单独注册bool就行,顺序无所谓:to_display.registerdef_(value:bool)-str:return是ifvalueelse否print(to_display(True))# 是(命中 bool,而不是 int)print(to_display(10))# 10(命中 int)不用再操心注册顺序,这是它比手写分支更省心的地方。两个进阶技巧用 register(type) 显式指定类型。如果你的运行环境不方便用参数注解(比如要给一个没有注解的 lambda 注册),可以把类型作为参数传给 register:to_display.register(bytes,lambdav:v.decode(utf-8,errorsreplace))给一个实现同时注册多种类型。链式调用多个 register 即可让几种类型共用一份逻辑:to_display.register(int)to_display.register(float)def_(value)-str:returnformat(value,,)# int、float 都走这里还有一个坑要知道:singledispatch只看第一个位置参数的类型。如果你的分发依赖第二个参数,或者依赖方法所在的实例(类里的方法),得改用functools.singledispatchmethod,用法类似但适配self在第一位的场景。小结functools.singledispatch按第一个参数的类型分发,把一坨isinstance分支拆成独立、可单独测试的实现。默认实现用singledispatch装饰做兜底,具体类型用.register注册,类型写在参数注解里。分派按 MRO「最具体优先」,子类自动命中父类实现,bool/int的顺序陷阱自动化解。只对第一个参数分派;要按self或方法分派,换singledispatchmethod。一句话记忆:当你发现一个函数里 isinstance 分支越堆越多,就该请出 singledispatch——加类型时只添新函数,永远不碰老代码。
返回列表