【译】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…

关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。

相关文章
相关标签/搜索