重載函數,即多個函數具有相同的名稱,但功能不同。例如一個重載函數fn,調用它的時候,要根據傳給函數的參數判斷調用哪個函數,并且執行相應的功能。
int area(int length, int breadth) {
return length * breadth;
}
float area(int radius) {
return 3.14 * radius * radius;
}
上例是用C++寫的代碼,函數area就是有兩個不同功能的重載函數,一個是根據參數length和breadth計算矩形的面積,另一個是根據參數radius(圓的半徑)計算圓的面積。如果用area(7)的方式調用函數area,就會實作第二個函數功能,當area(3, 4)時調用的是第一個函數。
為什麼Python中沒有重載函數
Python中本沒有重載函數,如果我們在同一命名空間中定義的多個函數是同名的,最後一個将覆寫前面的各函數,也就是函數的名稱隻能是唯一的。通過執行locals()和globals()兩個函數,就能看到該命名空間中已經存在的函數。
def area(radius):
return 3.14 * radius ** 2
>>> locals()
{
...
'area': <function area at 0x10476a440>,
...
}
定義了一個函數之後,執行locals()函數,傳回了一個字典,其中是本地命名空間中所定義所有變量,鍵是變量,值則是它的引用。如果有另外一個同名函數,就會将本地命名空間的内容進行更新,不會有兩個同名函數共存。是以,Python不支援重載函數,這是發明這個語言的設計理念,但是這并不能阻擋我們不能實作重載函數。下面就做一個試試。
在Python中實作重載函數
我們應該知道Python怎麼管理命名空間,如果我們要實作重載函數,必須:
- 在穩定的虛拟命名空間管理所定義的函數
- 根據參數調用合适的函數
為了簡化問題,我們将實作具有相同名稱的重載函數,它們的差別就是參數的個數。
封裝函數
建立一個名為Function的類,并重寫實作調用的__call__方法,再寫一個名為key的方法,它會傳回一個元組,這樣讓就使得此方法差別于其他方法。
from inspect import getfullargspec
class Function:
"""Function is a wrap over standard python function.
"""
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
"""when invoked like a function it internally invokes
the wrapped function and returns the returned value.
"""
return self.fn(*args, **kwargs)
def key(self, args=None):
"""Returns the key that will uniquely identify
a function (even when it is overloaded).
"""
# if args not specified, extract the arguments from the
# function definition
if args is None:
args = getfullargspec(self.fn).args
return tuple([
self.fn.__module__,
self.fn.__class__,
self.fn.__name__,
len(args or []),
])
在上面的代碼片段中,key方法傳回了一個元組,其中的元素包括:
- 函數所屬的子產品
- 函數所屬的類
- 函數名稱
- 函數的參數長度
在重寫的__call__方法中調用作為參數的函數,并傳回計算結果。這樣,執行個體就如同函數一樣調用,它的表現效果與作為參數的函數一樣。
def area(l, b):
return l * b
>>> func = Function(area)
>>> func.key()
('__main__', <class 'function'>, 'area', 2)
>>> func(3, 4)
12
在上面的舉例中,函數area作為Function執行個體化的參數,key()傳回的元組中,第一個元素是子產品的名稱__main__,第二個是類<class 'function'>,第三個是函數的名字area,第四個則是此函數的參數個數2。
從上面的示例中,還可以看出,調用執行個體func的方式,就和調用area函數一樣,提供參數3和4,就傳回12,前面調用area(3, 4)也是同樣結果。這種方式,會在後面使用裝飾器的時候很有用。
建構虛拟命名空間
我們所建構的虛拟命名空間,會儲存所定義的所有函數。
class Namespace(object):
"""Namespace is the singleton class that is responsible
for holding all the functions.
"""
__instance = None
def __init__(self):
if self.__instance is None:
self.function_map = dict()
Namespace.__instance = self
else:
raise Exception("cannot instantiate a virtual Namespace again")
@staticmethod
def get_instance():
if Namespace.__instance is None:
Namespace()
return Namespace.__instance
def register(self, fn):
"""registers the function in the virtual namespace and returns
an instance of callable Function that wraps the
function fn.
"""
func = Function(fn)
self.function_map[func.key()] = fn
return func
Namespace類中的方法register以函數fn為參數,在此方法内,利用fn建立了Function類的執行個體,還将它作為字典的值。那麼,方法register的傳回值,也是一個可調用對象,其功能與前面封裝的fn函數一樣。
def area(l, b):
return l * b
>>> namespace = Namespace.get_instance()
>>> func = namespace.register(area)
>>> func(3, 4)
12
用裝飾器做鈎子
我們已經定義了一個虛拟命名空間,并且可以向其中注冊一個函數,下面就需要一個鈎子,在該函數生命周期内調用它,為此使用Python的裝飾器。在Python中,裝飾器是一種封裝的函數,可以将它加到一個已有函數上,并不需要了解其内部結構。裝飾器接受函數fn作為參數,并且傳回另外一個函數,在這個函數被調用的時候,可以用args和kwargs為參數,并得到傳回值。
下面是一個簡單的封裝器示例:
import time
def my_decorator(fn):
"""my_decorator is a custom decorator that wraps any function
and prints on stdout the time for execution.
"""
def wrapper_function(*args, **kwargs):
start_time = time.time()
# invoking the wrapped function and getting the return value.
value = fn(*args, **kwargs)
print("the function execution took:", time.time() - start_time, "seconds")
# returning the value got after invoking the wrapped function
return value
return wrapper_function
@my_decorator
def area(l, b):
return l * b
>>> area(3, 4)
the function execution took: 9.5367431640625e-07 seconds
12
在上面的示例中,定義了名為my_decorator的裝飾器,并用它裝飾函數area,在互動模式中調用,列印出area(3,4)的執行時間。
裝飾器my_decorator裝飾了一個函數之後,當執行函數的時候,該裝飾器函數也每次都要調用,是以,裝飾器函數是一個理想的鈎子,借助它可以向前述定義的虛拟命名空間中注冊函數。下面建立一個名為overload的裝飾器,用它在虛拟命名空間注冊函數,并傳回一個可執行對象。
def overload(fn):
"""overload is the decorator that wraps the function
and returns a callable object of type Function.
"""
return Namespace.get_instance().register(fn)
overload裝飾器傳回Function執行個體,作為.register()的命名空間。現在,不論什麼時候通過overload調用函數,都會傳回.register(),即Function執行個體,并且,在調用的時候,__call__也會執行。
從命名空間中檢視函數
除通常的子產品類和名稱外,消除歧義的範圍是函數接受的參數數,是以我們在虛拟命名空間中定義了一個稱為get的方法,該方法接受Python命名空間中的函數(将是最後一個同名定義 - 因為我們沒有更改 Python 命名空間的預設行為)和調用期間傳遞的參數(我們的非義化因子),并傳回要調用的消除歧義函數。
此get函數的作用是決定調用函數的實作(如果重載)。擷取适合函數的過程非常簡單,從函數和參數建立使用key函數的唯一鍵(在注冊時完成),并檢視它是否存在于函數系統資料庫中,如果在,就執行擷取針對它存儲操作。
def get(self, fn, *args):
"""get returns the matching function from the virtual namespace.
return None if it did not fund any matching function.
"""
func = Function(fn)
return self.function_map.get(func.key(args=args))
在get函數中建立了Function的執行個體,它可以用key方法得到唯一的鍵,并且不會在邏輯上重複,然後使用這個鍵在函數系統資料庫中得到相應的函數。
調用函數
如上所述,每當被overload裝飾器裝飾的函數被調用時,類Function中的方法__call__也被調用,進而通過命名空間的get函數得到恰當的函數,實作重載函數功能。__call__方法的實作如下:
def __call__(self, *args, **kwargs):
"""Overriding the __call__ function which makes the
instance callable.
"""
# fetching the function to be invoked from the virtual namespace
# through the arguments.
fn = Namespace.get_instance().get(self.fn, *args)
if not fn:
raise Exception("no matching function found.")
# invoking the wrapped function and returning the value.
return fn(*args, **kwargs)
這個方法從虛拟命名空間中得到恰當的函數,如果它沒有找到,則會發起異常。
重載函數實作
将上面的代碼規整到一起,定義兩個名字都是area的函數,一個計算矩形面積,另一個計算圓的面積,兩個函數均用裝飾器overload裝飾。
@overload
def area(l, b):
return l * b
@overload
def area(r):
import math
return math.pi * r ** 2
>>> area(3, 4)
12
>>> area(7)
153.93804002589985
當我們給調用的area傳一個參數時,傳回圓的面積,兩個參數時則計算了矩形面積,這樣就實作了重載函數area。
結論
Python不支援函數重載,但通過使用正常的文法,我們找到了它的解決方案。我們使用修飾器和使用者維護的命名空間來重載函數,并使用參數數作為消除歧義因素。還可以使用參數的資料類型(在修飾中定義)來消除歧義—— 它允許具有相同參數數但不同類型的函數重載。重載的粒度隻受函數getfullargspec和我們的想象力的限制。更整潔、更簡潔、更高效的方法也可用于上述構造。