文章地址:Python 函數(shù)實現(xiàn)重載
單分派泛函數(shù)
假如你想在交互模式下打印出美觀的對象顾犹,那么標準庫中的 pprint.pprint()
函數(shù)或許是一個不錯的選擇郁季。但是,如果你想 DIY 一個自己看著舒服的打印模式,那么你很可能會寫一長串的 if/else 語句虑稼,來判斷傳進來對象的類型。
def fprint(obj):
if isinstance(obj, list):
print_list(obj)
elif isinstance(obj, tuple):
print_tuple(obj)
elif isinstance(obj, str):
print_str(obj)
這樣做固然沒有錯势木,但是太多的 if 語句使得代碼不易擴展蛛倦,而且代碼可讀性也要大打折扣。
他山之石
首先讓我們先來看看其他語言會怎樣處理這樣的問題:
Java 支持方法重載啦桌,我們可以編寫同名方法溯壶,但是這些方法的參數(shù)要不一樣及皂,主要體現(xiàn)在參數(shù)個數(shù)與參數(shù)類型方面。下面我們重載了 fprint()
這個靜態(tài)方法且改,調(diào)用 fprint()
方法時验烧,如果傳進來的參數(shù)是字符串,那么就調(diào)用第一個方法又跛;如果傳進來的參數(shù)是整型碍拆,那么就調(diào)用第二個方法。
public class Overriding {
// 方法一
public static void fprint(String Astring){
System.out.println("我是一個字符串");
System.out.println(Astring);
}
// 方法二
public static void fprint(int Aint){
System.out.println("我是一個整型");
System.out.println(Aint);
}
public static void main(String[] args){
fprint("Hello, Python.");
fprint(666);
}
}
輸出結(jié)果:
我是一個字符串
Hello, Python.
我是一個整型
666
Python 的解決方案
Python 通過單分派泛函數(shù)部分支持了方法重載慨蓝。
官方文檔是這樣定義泛函數(shù)以及單分派函數(shù)的:
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.
也就是說單分派泛函數(shù)(single dispatch)可以根據(jù)第一個參數(shù)的類型感混,來判斷執(zhí)行哪一個函數(shù)體。
那么我們使用 singledispatch 重寫上面的例子:
- 首先我們要從functools 中導入 singledispatch
from functools import singledispatch
-
singledispatch 是作為裝飾器使用的函數(shù)礼烈。裝飾器是 Python 中的語法糖弧满,@singledispatch 實際上相當于 singledispatch(fprint),這里我們并不關(guān)心 singledispatch 的內(nèi)部實現(xiàn)此熬,我們只需知道 singledispatch 可以實現(xiàn)分派機制就行庭呜。NotImplemented 是 Python 中的內(nèi)置常量,提醒我們沒有實現(xiàn)某個功能摹迷。注意這與 NotImplementedError 有天壤之別疟赊,后者會導致異常出現(xiàn),終止程序峡碉。當調(diào)用
fprint()
函數(shù)時近哟,如果參數(shù)的類型沒有被注冊,那么默認會執(zhí)行使用 @singledispatch 裝飾的函數(shù)鲫寄。@singledispatch def fprint(obj): return NotImplemented
我們使用 @<主函數(shù)名>.register(type) 來裝飾專門函數(shù)吉执。要注意分派函數(shù)可以有任意多個參數(shù),但是調(diào)用函數(shù)時執(zhí)行哪一部分功能只由函數(shù)第一個參數(shù)決定地来,也就是由 register 中聲明的參數(shù)類型決定戳玫。 而對于專門函數(shù)來說,函數(shù)名是無關(guān)緊要的未斑,使用 _ 更加簡潔明了咕宿。
@singledispatch
def fprint(obj):
return NotImplemented
@fprint.register(str)
def _(obj):
print('我是一個字符串')
print(obj)
@fprint.register(int)
def _(obj):
print('我是一個整型')
print(obj)
- Python 3.7 中新增了一個功能:即使用 type annotions 來注明第一個參數(shù)的類型。打印結(jié)果蜡秽,與使用裝飾器參數(shù)得到的結(jié)果相同府阀。
@fprint.register
def _(obj:str):
print('我是一個字符串')
print(obj)
最后我們對代碼進行測試,結(jié)果符合我們的預期:
>>> fprint('Nice to meet you, Java')
我是一個字符串
Nice to meet you, Java
>>> fprint(999)
我是一個整型
999
>>> fprint((12, 4))
NotImplemented
更復雜的例子
上面就是 single dispatch 的基本用法芽突,下面讓我們看一個稍微復雜點的例子试浙。
想象,你需要一個自定義的打印函數(shù)寞蚌,又不想過多地使用 if/else 分支田巴,那么你可以應用剛學到的 single dispatch 來解決問題钠糊。
- 首先要導入我們需要的庫,這里我們用到了幾個抽象基類壹哺,整數(shù) Integral 和 可變序列 MutableSequence抄伍。使用抽象基類,可以使得我們的程序可拓展性更強斗躏。使用 Integral 注冊的函數(shù)不僅支持常規(guī)的 int 類型逝慧,還支持Integral 的子類或者注冊為 Integral 的虛擬子類,甚至可以支持實現(xiàn)了 Integral “協(xié)議” 的類型啄糙。這充分體現(xiàn)了Python “鴨子類型” 的強大之處笛臣。可變序列也是如此隧饼,不僅支持常規(guī)的 list 類型沈堡,還支持符合要求的自定義類型。
from functools import singledispatch
from numbers import Integral
from collections.abc import MutableSequence
- 其次燕雁,我們定義默認行為诞丽。即沒有被注冊的類型,會執(zhí)行下面的函數(shù)拐格。這里僧免,為了方便起見,我們直接打印對象的類型與內(nèi)容捏浊。
@singledispatch
def pprint(obj):
print(f'({obj.__class__.__name__}) {obj}')
>>> pprint('微信公眾號:Python高效編程')
(str) 微信公眾號:Python高效編程
- 第一個函數(shù)使用了 type annotations懂衩,注冊為 Integral 類型。第二個函數(shù)注冊為 float 類型金踪,打印的時候小數(shù)點后保留兩位浊洞。
@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
- 我們還可以通過堆積(類型注冊)裝飾器,來實現(xiàn)對多種類型的支持胡岔。下面這個函數(shù)就支持三種類型法希,分別是元組,集合靶瘸,可變序列苫亦。
@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
- 最后我們支持了字典類型:
@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':'關(guān)注轉(zhuǎn)發(fā)', 'part3': 666}
>>> pprint(a)
-------dict--------
key value
(str)part1 -> (str)Python高效編程
(str)part2 -> (str)關(guān)注轉(zhuǎn)發(fā)
(str)part3 -> (int)666
講完上面的例子,我們再補充一個小用法:
- 假如我們想知道 pprint() 函數(shù)支持哪些類型怨咪,我們該怎么做呢屋剑?
pprint.registry 返回類型與函數(shù)地址的鍵值對,調(diào)用 keys() 方法獲取 pprint() 函數(shù)支持的類型惊暴。
>>> pprint.registry.keys()
dict_keys([<class 'object'>,
<class 'numbers.Integral'>,
<class 'float'>,
<class 'collections.abc.MutableSequence'>,
<class 'set'>, <class 'tuple'>, <class 'dict'>])
總結(jié)
這篇文章饼丘,我們從一個簡單例子切入趁桃,介紹了 singledispatch 的簡單用法與使用案例辽话。如果你發(fā)現(xiàn)你的代碼需要使用分派函數(shù)肄鸽,不妨嘗試這種代碼風格。如果想深入了解 singledispatch 的用法油啤,不妨去 PEP 443 和functools
docs 一探究竟典徘。