:用單分派消滅一堆 isinstance 判斷)
Python functools.singledispatch 實戰(zhàn):用單分派消滅一堆 isinstance 判斷寫 Python 時你大概率見過這種函數(shù):開頭一長串if isinstance(x, int): ... elif isinstance(x, str): ... elif isinstance(x, list): ...。每加一種類型就往里塞一個分支,函數(shù)越來越臃腫,改一處怕碰壞別處。標(biāo)準(zhǔn)庫的functools.singledispatch就是專門治這個的——它讓你按參數(shù)類型把邏輯拆成獨立的函數(shù),新增類型時不用動老代碼。這篇手把手帶你用起來。先看看 isinstance 分支有多難維護假設(shè)要寫一個把不同類型序列化成「適合展示的字符串」的函數(shù):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)問題很明顯:所有類型的邏輯擠在一個函數(shù)里;分支順序還暗藏陷阱(bool是int的子類,放后面永遠(yuǎn)命中不了);想讓第三方新增一種類型的處理,只能改這個函數(shù)的源碼。用 singledispatch 拆開singledispatch把「根據(jù)第一個參數(shù)的類型分發(fā)」這件事交給標(biāo)準(zhǔn)庫。先定義一個泛型函數(shù)作為默認(rèn)實現(xiàn),再用.register為具體類型注冊各自的實現(xiàn):fromfunctoolsimportsingledispatchsingledispatchdefto_display(value)-str:# 默認(rèn)實現(xiàn):兜底所有沒單獨注冊的類型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)注意幾個細(xì)節(jié):被singledispatch裝飾的是默認(rèn)/兜底實現(xiàn);每個register的函數(shù)名都可以寫成_,因為我們從不直接調(diào)用它們,只通過to_display(x)觸發(fā)分派。類型直接寫在參數(shù)注解里(Python 3.7 支持這種寫法),讀起來很清爽。調(diào)用方式和普通函數(shù)完全一樣: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}(走默認(rèn)實現(xiàn))子類會自動匹配最近的父類singledispatch按 MRO(方法解析順序)查找,注冊父類后,子類實例會自動命中父類的實現(xiàn)。這對處理一族類型特別省事:fromcollections.abcimportSequenceto_display.registerdef_(value:Sequence)-str:return | .join(str(v)forvinvalue)# tuple 是 Sequence 的子類,自動走上面這個實現(xiàn)print(to_display((x,y)))# x | y如果同時注冊了list和它的父類Sequence,list實例會優(yōu)先命中更具體的list實現(xiàn)——「最具體的優(yōu)先」,這正是我們想要的。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) 顯式指定類型。如果你的運行環(huán)境不方便用參數(shù)注解(比如要給一個沒有注解的 lambda 注冊),可以把類型作為參數(shù)傳給 register:to_display.register(bytes,lambdav:v.decode(utf-8,errorsreplace))給一個實現(xiàn)同時注冊多種類型。鏈?zhǔn)秸{(diào)用多個 register 即可讓幾種類型共用一份邏輯:to_display.register(int)to_display.register(float)def_(value)-str:returnformat(value,,)# int、float 都走這里還有一個坑要知道:singledispatch只看第一個位置參數(shù)的類型。如果你的分發(fā)依賴第二個參數(shù),或者依賴方法所在的實例(類里的方法),得改用functools.singledispatchmethod,用法類似但適配self在第一位的場景。小結(jié)functools.singledispatch按第一個參數(shù)的類型分發(fā),把一坨isinstance分支拆成獨立、可單獨測試的實現(xiàn)。默認(rèn)實現(xiàn)用singledispatch裝飾做兜底,具體類型用.register注冊,類型寫在參數(shù)注解里。分派按 MRO「最具體優(yōu)先」,子類自動命中父類實現(xiàn),bool/int的順序陷阱自動化解。只對第一個參數(shù)分派;要按self或方法分派,換singledispatchmethod。一句話記憶:當(dāng)你發(fā)現(xiàn)一個函數(shù)里 isinstance 分支越堆越多,就該請出 singledispatch——加類型時只添新函數(shù),永遠(yuǎn)不碰老代碼。