Python中singledispatch裝飾器實現函數重載


本文參照"流暢的Python"這本書有關於singledispatch實現函數重載的闡述[1].

假設我們現在要實現一個函數, 功能是將一個對象轉換成html格式的字符串. 怎么重載呢?

你可能會想, 用什么裝飾器, Python不是動態類型么, 寫成如下這樣不就重載了嘛? 

def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

這個函數可以接受任意類型的參數. 你看,這不就重載了么?

如果我想讓不同類型的對象有不同形式的html字符串呢 ? 你可能會說, 那就加個類型判斷唄! 像下面這樣.

def ifelseHtmlize(obj):
    if isinstance(obj, str):
        content = html.escape(repr(obj))
        content = '<pre>{}</pre>'.format(content)
    elif isinstance(obj, numbers.Integral):
        content = "<pre>{0} (0x{0:x})</pre>".format(obj)
    
    return content

額...這樣當然可以實現上述需求. 然而, 當每種類型的處理邏輯比較復雜時, 以上方法大大增加了函數的篇幅, 影響可讀性和可維護性. 你可能又會說, 那把每個分支抽象成函數就好了, 這樣ifelseHtmlize函數的代碼量就少了. 但是這樣需要抽象出很多名字不一樣的函數, 維護起來還是不太容易. 

因此, 我們需要尋求一種方法, 讓每個重載函數能夠關注自身需要處理的類型. 而且可以很簡單的添加和去除. 於是就有了singledispatch這個裝飾器. 先簡單介紹下裝飾器, 裝飾器本質上是個函數, 其輸入是一個函數, 返回值也是個函數. 把"@裝飾器"放到哪個函數的頭頂, 哪個函數就會作為參數輸入到裝飾器, 返回的函數再賦值給被裝飾的函數. 這個技術的目的是對被裝飾的函數做一些其他的手腳, 使其具有一些需要的特性. 例如singledispatch是Python內置眾多裝飾器之一, 就是為了達到函數重載的目的.

如何用singledispatch實現重載呢, 直接上代碼:

@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace("\n", "<br>\n")
    return '<pre>{}</pre>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return "<pre>{0} (0x{0:x})</pre>".format(n)

乍一看腦殼疼. 簡化一下, 如果去掉函數頭頂上的裝飾器, 再將這三個函數看作是一個函數名, 這和C++里的重載是類似的. 什么? 沒學過C++, 那這句話當我沒說, 接着看.

首先看第一個函數, 之前實現的htmlize函數被singledispatch裝飾了, 意味着htmlize再也不是原來那個單純的htmlize了. 現在的htmlize函數, 是被傳入singledispatch后再返回出來的那個對象/函數. 具體的我們看一下singledispatch對htmlize都干了什么.

def singledispatch(func):
    # 用來記錄 類型->函數
    registry = {}
    ...
    
    # 用來獲得 指定類型的重載函數
    def dispatch(cls):
    ...
    
    # 注冊新的 類型->函數
    def register(cls, func=None):
    ...
    
    # 重載函數入口
    def wrapper(*args, **kw)
    ...
    
    # 默認的重載函數
    registry[object] = func
    wrapper.register = register
    wrapper.dispatch = dispatch
    ...

    # 返回 wrapper供使用這調用重載函數
    return wrapper    

在singledispatch函數中, 參數func就是原始的htmlize函數. 然后先定義了一個字典registry, 看這個名字大概能猜出這是干嘛的. 對了, 就是注冊用的, 注冊啥呢? 當然是新的重載函數. 接着我們跳過中間幾個函數, 先看下面一句registry[object] = func, 這將object類型對應的重載函數設置為func, 也就是傳入的htmlize. 為什么是object呢? 這里埋個伏筆, 后面再講. 現在說一下跳過的那三個函數, 實際上是三個閉包:

dispatch: 輸入參數cls是類型, 根據給定的cls去registry中找對應的重載函數, 然后返回給調用者. 后面會見到這個函數是如何被調用的.

register: 用來注冊新的重載函數. 核心的功能就是向registry中添加新的 類型->函數.

wrapper: 重載函數調用的入口, 負責執行正確的重載函數, 並返回結果.

最后對wrapper函數賦值了兩個屬性register和dispatch, 分別是同名的兩個閉包, 接着返回wrapper成為新的htmlize. 什么, 沒聽說過直接給函數屬性賦值? 看PEP 232 -- Function Attributes. 這里重點關注一下register, 有了它, 用戶可以通過wrapper調用這個閉包. 意味着用戶可以用register注冊新的重載函數. 我們最終的目的要呼之欲出啦!

到這里只是對htmlize函數進行重載的使能. 接下來就可以定義htmlize的重載函數了. 直接上代碼:

@htmlize.register(str)
def htmlize4str(text):
    content = html.escape(text).replace("\n", "<br>\n")
    return '<pre>{}</pre>'.format(content)

@htmlize.register(numbers.Integral)
def htmlize4Integral(n):
    return "<pre>{0} (0x{0:x})</pre>".format(n)
 
        

上面兩個函數就是htmlize的兩個重載函數, 分別用於處理字符串和Integral類型. 這兩個函數均被htmlize.register(cls)返回的函數裝飾. 我們知道, 上述singledispatch返回的wrapper會重新賦值給htmlize, 所以調用htmlize.register(cls)即是調用閉包register. 我們以htmlize.register(str)為例, 看看閉包register干了什么:

1     def register(cls, func=None):
2         ...
3         if func is None:
4             return lambda f: register(cls, f)
5         registry[cls] = func
6         ...
7         return func

調用register(str), 因為func是None, 所以進入分支, 直接返回一個函數lambda f: register(str, f). 返回之后的函數作為裝飾器, 對htmlize4str函數進行裝飾. 於是htmlize4str函數作為lambda表達式中的f, 實際上調用了register(str, htmlize4str). 於是, 又回到了上述函數, 這次func==htmlize4str非None, 於是str->htmlize4str得以注冊, 最后返回htmlize4str.

以上, 就完成了注冊重載函數的過程了. 那如何實現傳入htmlize不同參數, 執行不同的函數呢. 比如調用htmlize("23333"), 如何定位到htmlize4str("23333")呢? 現在回憶一下singledispatch裝飾的htmlize現在是什么? 是wrapper閉包啊, 所以調用htmlize("23333"), 即調用wrapper("23333"). 我們看看wrapper做了什么:

1     def wrapper(*args, **kw):
2         return dispatch(args[0].__class__)(*args, **kw)

wrapper將輸入的第一個參數的__class__, 即類型輸入到dispatch. 我們之前提到過, dispatch這個函數用於找到指定類型的重載函數. dispatch返回后執行這個重載函數, 再將結果返回給調用者. 例如, wrapper("23333")首先調用dispatch(str), 因為"23333"的類型是str, 找到對應的重載函數, 即htmlize4str, 然后再調用htmlize4str("23333"). 實現重載啦啦啦! 而且我們發現重載函數的函數名, 對於調用htmlize是透明的, 根本用不到. 所以重載的函數名可以用_替代, 這樣更好維護代碼.

最后我們看看dispatch這個閉包干了些什么:

 1     def dispatch(cls):
 2         """generic_func.dispatch(cls) -> <function implementation>
 3 
 4         Runs the dispatch algorithm to return the best available implementation
 5         for the given *cls* registered on *generic_func*.
 6 
 7         """
 8         ...
 9             try:
10                 impl = registry[cls]
11             except KeyError:
12                 impl = _find_impl(cls, registry)
13         ...
14         return impl

核心的功能就是到registry里找對應的cls類型的重載函數, 然后返回就行了. 那如果沒找到呢? 比如我調用了htmlize({1, 2, 3}), 這時cls是list類型, 在registry里沒有找到對應的重載函數咋辦呢? 在上述代碼中, 捕捉了registry拋出的KeyError異常, 即在沒有找到時執行_find_impl(cls, registry), 這又是干嘛的呢? 這里不展開講了, 我也展不開. 總之, 用一句話來說: 找到registry中和cls類型最匹配的類型, 然后返回其重載函數.

看看現在我們的registry里有哪些類型呢? str, numbers.Integral. 哦!!! 還有object (回憶一下, 在singledispatch中有這么一句: registry[object] = func). Python里所有類型都繼承object類型, 於是返回registry中object對應的重載函數, 即最原始的htmlize. 

以上.

 

[1] Ramalho, Luciano. Fluent Python : clear, concise, and effective programming. Sebastopol, CA : O'Reilly, 2015.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM