做者:Arpitpython
翻譯:老齊bash
重載函數,即多個函數具備相同的名稱,但功能不一樣。例如一個重載函數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)
時調用的是第一個函數。app
Python中本沒有重載函數,若是咱們在同一命名空間中定義的多個函數是同名的,最後一個將覆蓋前面的各函數,也就是函數的名稱只能是惟一的。經過執行locals()
和globals()
兩個函數,就能看到該命名空間中已經存在的函數。ide
def area(radius):
return 3.14 * radius ** 2
>>> locals()
{
...
'area': <function area at 0x10476a440>,
...
}
複製代碼
定義了一個函數以後,執行locals()
函數,返回了一個字典,其中是本地命名空間中所定義全部變量,鍵是變量,值則是它的引用。若是有另一個同名函數,就會將本地命名空間的內容進行更新,不會有兩個同名函數共存。因此,Python不支持重載函數,這是發明這個語言的設計理念,可是這並不能阻擋咱們不能實現重載函數。下面就作一個試試。函數
咱們應該知道Python怎麼管理命名空間,若是咱們要實現重載函數,必須:fetch
爲了簡化問題,咱們將實現具備相同名稱的重載函數,它們的區別就是參數的個數。ui
建立一個名爲Function
的類,並重寫實現調用的__call__
方法,再寫一個名爲key
的方法,它會返回一個元組,這樣讓就使得此方法區別於其餘方法。spa
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
和咱們的想象力的限制。更整潔、更簡潔、更高效的方法也可用於上述構造。
原文連接:arpitbhayani.me/blogs/funct…
關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。