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