Python 函數如何實現“重載”

單分派泛函數

假如你想在交互模式下打印出美觀的對象,那麼標準庫中的 pprint.pprint() 函數或許是一個不錯的選擇。但是,如果你想 DIY 一個自己看著舒服的打印模式,那麼你很可能會寫一長串的 if/else 語句,來判斷傳進來對象的類型。

Python 函數如何實現“重載”

這樣做固然沒有錯,但是太多的 if 語句使得代碼不易擴展,而且代碼可讀性也要大打折扣。

他山之石

首先讓我們先來看看其他語言會怎樣處理這樣的問題:

Java 支持方法重載,我們可以編寫同名方法,但是這些方法的參數要不一樣,主要體現在參數個數與參數類型方面。下面我們重載了 fprint() 這個靜態方法,調用 fprint() 方法時,如果傳進來的參數是字符串,那麼就調用第一個方法;如果傳進來的參數是整型,那麼就調用第二個方法。

Python 函數如何實現“重載”

輸出結果:

我是一個字符串
Hello, Python.
我是一個整型
666

Python 的解決方案

Python 通過單分派泛函數部分支持了方法重載。

官方文檔是這樣定義泛函數以及單分派函數的:

A generic function is composed of multiple functions implementing the same operation for different types. Which implementation should be used during a call is determined by the dispatch algorithm. When the implementation is chosen based on the type of a single argument, this is known as single dispatch.

也就是說單分派泛函數(single dispatch)可以根據第一個參數的類型,來判斷執行哪一個函數體。

那麼我們使用 singledispatch 重寫上面的例子:

首先我們要從functools 中導入 singledispatch

from functools import singledispatch

singledispatch 是作為裝飾器使用的函數。裝飾器是 Python 中的語法糖,@singledispatch 實際上相當於 singledispatch(fprint),這裡我們並不關心 singledispatch 的內部實現,我們只需知道 singledispatch 可以實現分派機制就行。NotImplemented 是 Python 中的內置常量,提醒我們沒有實現某個功能。注意這與 NotImplementedError 有天壤之別,後者會導致異常出現,終止程序。當調用 fprint() 函數時,如果參數的類型沒有被註冊,那麼默認會執行使用 @singledispatch 裝飾的函數。

@singledispatch
def fprint(obj):
return NotImplemented

我們使用 @.register(type) 來裝飾專門函數。要注意分派函數可以有任意多個參數,但是調用函數時執行哪一部分功能只由函數第一個參數決定,也就是由 register 中聲明的參數類型決定。 而對於專門函數來說,函數名是無關緊要的,使用 _ 更加簡潔明瞭。

@singledispatch
def fprint(obj):
return NotImplemented
@fprint.register(str)
def _(obj):
print('我是一個字符串')
print(obj)
@fprint.register(int)
def _(obj):
print('我是一個整型')
print(obj)
  1. Python 3.7 中新增了一個功能:即使用 type annotions 來註明第一個參數的類型。打印結果,與使用裝飾器參數得到的結果相同。
@fprint.register
def _(obj:str):
print('我是一個字符串')
print(obj)

最後我們對代碼進行測試,結果符合我們的預期:

>>> fprint('Nice to meet you, Java')
我是一個字符串
Nice to meet you, Java
>>> fprint(999)
我是一個整型
999
>>> fprint((12, 4))
NotImplemented

更復雜的例子

上面就是 single dispatch 的基本用法,下面讓我們看一個稍微複雜點的例子。

想象,你需要一個自定義的打印函數,又不想過多地使用 if/else 分支,那麼你可以應用剛學到的 single dispatch 來解決問題。

  1. 首先要導入我們需要的庫,這裡我們用到了幾個抽象基類,整數 Integral 和 可變序列 MutableSequence。使用抽象基類,可以使得我們的程序可拓展性更強。使用 Integral 註冊的函數不僅支持常規的 int 類型,還支持Integral 的子類或者註冊為 Integral 的虛擬子類,甚至可以支持實現了 Integral “協議” 的類型。這充分體現了Python “鴨子類型” 的強大之處。可變序列也是如此,不僅支持常規的 list 類型,還支持符合要求的自定義類型。
from functools import singledispatch
from numbers import Integral

from collections.abc import MutableSequence
  1. 其次,我們定義默認行為。即沒有被註冊的類型,會執行下面的函數。這裡,為了方便起見,我們直接打印對象的類型與內容。
@singledispatch
def pprint(obj):
print(f'({obj.__class__.__name__}) {obj}')
>>> pprint('微信公眾號:Python高效編程')
(str) 微信公眾號:Python高效編程
  1. 第一個函數使用了 type annotations,註冊為 Integral 類型。第二個函數註冊為 float 類型,打印的時候小數點後保留兩位。
@pprint.register
def _(obj:Integral):
print(f'({obj.__class__.__name__}) {obj}')
@pprint.register(float)
def _(obj):
print(f'({obj.__class__.__name__}) {obj:.2f}')
>>> pprint(666)
(int) 666
>>> pprint(66.6457)
(float) 66.65
  1. 我們還可以通過堆積(類型註冊)裝飾器,來實現對多種類型的支持。下面這個函數就支持三種類型,分別是元組,集合,可變序列。
Python 函數如何實現“重載”

@pprint.register(tuple)
@pprint.register(set)
@pprint.register(MutableSequence)
def _(obj):
print(f'{"-"*7}{obj.__class__.__name__}{"-"*8}')
print(f'index type value')
for index, value in enumerate(obj):
print(f'{index:^6}->{type(value).__name__:<8}: {value}')
>>> a = [[1, 3, 4], 'name', 5, 6]
>>> pprint(a)
-------list--------
index type value
0 ->list : [1, 3, 4]
1 ->str : name
2 ->int : 5
3 ->int : 6
>>> b = {1, 3, 4}
>>> pprint(b)
-------set--------

index type value
0 ->int : 1
1 ->int : 3
2 ->int : 4
  1. 最後我們支持了字典類型:
@pprint.register(dict)
def _(obj):
print(f'{"-"*7}{obj.__class__.__name__}{"-"*8}')
print(' key value')
for k, v in sorted(obj.items()):
print(f'({type(k).__name__}){k:<6} -> ({type(v).__name__}){v:<6}')
>>> a = {'part1': "Python高效編程",'part2':'關注轉發', 'part3': 666}
>>> pprint(a)
-------dict--------
key value
(str)part1 -> (str)Python高效編程
(str)part2 -> (str)關注轉發
(str)part3 -> (int)666

講完上面的例子,我們再補充一個小用法:

  • 假如我們想知道 pprint() 函數支持哪些類型,我們該怎麼做呢?

pprint.registry 返回類型與函數地址的鍵值對,調用 keys() 方法獲取 pprint() 函數支持的類型。

>>> pprint.registry.keys()
dict_keys([<class>,
<class>,
<class>,
<class>,
<class>, <class>, <class>])
/<class>/<class>/<class>/<class>/<class>/<class>/<class>

總結

這篇文章,我們從一個簡單例子切入,介紹了 singledispatch 的簡單用法與使用案例。如果你發現你的代碼需要使用分派函數,不妨嘗試這種代碼風格。如果想深入瞭解 singledispatch 的用法,不妨去 PEP 443 和functools docs 一探究竟。

Python 函數如何實現“重載”

Python 函數如何實現“重載”

Python高效編程


分享到:


相關文章: