天天看點

Python | 實作重載函數

作者:VT聊球

重載函數,即多個函數具有相同的名稱,但功能不同。例如一個重載函數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和我們的想象力的限制。更整潔、更簡潔、更高效的方法也可用于上述構造。