• 【译】Python如何实现重载函数


    重载函数,即多个函数具有相同的名称,但功能不同。例如一个重载函数 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 和我们的想象力的限制。更整洁、更简洁、更高效的方法也可用于上述构造。

  • 相关阅读:
    【HDU1166】敌兵布阵-单点修改和区间求和
    【HDU1166】敌兵布阵-单点修改和区间求和
    【Ural1028】Stars-线段树和树状数组入门题
    【Ural1028】Stars-线段树和树状数组入门题
    【NOIP2014提高组T3】飞扬的小鸟-完全背包
    【NOIP2014提高组T3】飞扬的小鸟-完全背包
    【POJ2528】Mayor's Posters-线段树+离散化
    【POJ2528】Mayor's Posters-线段树+离散化
    perl use utf8
    encode_utf8 把字符编码成字节 decode_utf8解码UTF-8到字符
  • 原文地址:https://www.cnblogs.com/pythoncxy/p/12365823.html
Copyright © 2020-2023  润新知