【譯】Python如何實現重載函數

做者: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中沒有重載函數

Python中本沒有重載函數,若是咱們在同一命名空間中定義的多個函數是同名的,最後一個將覆蓋前面的各函數,也就是函數的名稱只能是惟一的。經過執行locals()globals()兩個函數,就能看到該命名空間中已經存在的函數。app

def area(radius):
  return 3.14 * radius ** 2

>>> locals()
{
  ...
  'area': <function area at 0x10476a440>,
  ...
}
複製代碼

定義了一個函數以後,執行locals()函數,返回了一個字典,其中是本地命名空間中所定義全部變量,鍵是變量,值則是它的引用。若是有另一個同名函數,就會將本地命名空間的內容進行更新,不會有兩個同名函數共存。因此,Python不支持重載函數,這是發明這個語言的設計理念,可是這並不能阻擋咱們不能實現重載函數。下面就作一個試試。ide

在Python中實現重載函數

咱們應該知道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函數同樣,提供參數34,就返回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做爲參數,而且返回另一個函數,在這個函數被調用的時候,能夠用argskwargs爲參數,並獲得返回值。

下面是一個簡單的封裝器示例:

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…

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

相關文章
相關標籤/搜索