英文:https://arpitbhayani.me/blogs/function-overloading

作者:arprit

译者:豌豆花下猫(“Python猫”公众号作者)

声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。

函数重载指的是有多个同名的函数,但是它们的签名或实现却不同。当调用一个重载函数 fn 时,程序会检验传递给函数的实参/形参,并据此而调用相应的实现。

  1. int area(int length, int breadth) {
  2. return length * breadth;
  3. }
  4. float area(int radius) {
  5. return 3.14 * radius * radius;
  6. }

在以上例子中(用 c++ 编写),函数 area 被重载了两个实现。第一个函数接收两个参数(都是整数),表示矩形的长度和宽度,并返回矩形的面积。另一个函数只接收一个整型参数,表示圆的半径。

当我们像 area(7) 这样调用函数 area 时,它会调用第二个函数,而 area(3,4) 则会调用第一个函数。

为什么 Python 中没有函数重载?

Python 不支持函数重载。当我们定义了多个同名的函数时,后面的函数总是会覆盖前面的函数,因此,在一个命名空间中,每个函数名仅会有一个登记项(entry)。

Python猫注:这里说 Python 不支持函数重载,指的是在不用语法糖的情况下。使用 functools 库的 singledispatch 装饰器,Python 也可以实现函数重载。原文作者在文末的注释中专门提到了这一点。

通过调用 locals() 和 globals() 函数,我们可以看到 Python 的命名空间中有什么,它们分别返回局部和全局命名空间。

  1. def area(radius):
  2. return 3.14 * radius ** 2
  3. >>> locals()
  4. {
  5. ...
  6. 'area': <function area at 0x10476a440>,
  7. ...
  8. }

在定义一个函数后,接着调用 locals() 函数,我们会看到它返回了一个字典,包含了定义在局部命名空间中的所有变量。字典的键是变量的名称,值是该变量的引用/值。

当程序在运行时,若遇到另一个同名函数,它就会更新局部命名空间中的登记项,从而消除两个函数共存的可能性。因此 Python 不支持函数重载。这是在创造语言时做出的设计决策,但这并不妨碍我们实现它,所以,让我们来重载一些函数吧。

在 Python 中实现函数重载

我们已经知道 Python 是如何管理命名空间的,如果想要实现函数重载,就需要这样做:

  • 维护一个虚拟的命名空间,在其中管理函数定义
  • 根据每次传递的参数,设法调用适当的函数

为了简单起见,我们在实现函数重载时,通过不同的参数数量来区分同名函数。

把函数封装起来

我们创建了一个名为Function的类,它可以封装任何函数,并通过重写的__call__方法来调用该函数,还提供了一个名为key的方法,该方法返回一个元组,使该函数在整个代码库中是唯一的。

  1. from inspect import getfullargspec
  2. class Function(object):
  3. """Function类是对标准的Python函数的封装"""
  4. def __init__(self, fn):
  5. self.fn = fn
  6. def __call__(self, *args, **kwargs):
  7. """当像函数一样被调用时,它就会调用被封装的函数,并返回该函数的返回值"""
  8. return self.fn(*args, **kwargs)
  9. def key(self, args=None):
  10. """返回一个key,能唯一标识出一个函数(即便是被重载的)"""
  11. # 如果不指定args,则从函数的定义中提取参数
  12. if args is None:
  13. args = getfullargspec(self.fn).args
  14. return tuple([
  15. self.fn.__module__,
  16. self.fn.__class__,
  17. self.fn.__name__,
  18. len(args or []),
  19. ])

在上面的代码片段中,key函数返回一个元组,该元组唯一标识了代码库中的函数,并且记录了:

  • 函数所属的模块
  • 函数所属的类
  • 函数名
  • 函数接收的参数量

被重写的__call__方法会调用被封装的函数,并返回计算的值(这没有啥特别的)。这使得Function的实例可以像函数一样被调用,并且它的行为与被封装的函数完全一样。

  1. def area(l, b):
  2. return l * b
  3. >>> func = Function(area)
  4. >>> func.key()
  5. ('__main__', <class 'function'>, 'area', 2)
  6. >>> func(3, 4)
  7. 12

在上面的例子中,函数area被封装在Function中,并被实例化成func。key() 返回一个元组,其第一个元素是模块名__main__,第二个是类<class 'function'>,第三个是函数名area,而第四个则是该函数接收的参数数量,即 2。

这个示例还显示出,我们可以像调用普通的 area函数一样,去调用实例 func,当传入参数 3 和 4时,得到的结果是 12,这正是调用 area(3,4) 时会得到的结果。当我们接下来运用装饰器时,这种行为将会派上用场。

构建虚拟的命名空间

我们要创建一个虚拟的命名空间,用于存储在定义阶段收集的所有函数。

由于只有一个命名空间/注册表,我们创建了一个单例类,并把函数保存在字典中。该字典的键不是函数名,而是我们从 key 函数中得到的元组,该元组包含的元素能唯一标识出一个函数。

通过这样,我们就能在注册表中保存所有的函数,即使它们有相同的名称(但不同的参数),从而实现函数重载。

  1. class Namespace(object):
  2. """Namespace是一个单例类,负责保存所有的函数"""
  3. __instance = None
  4. def __init__(self):
  5. if self.__instance is None:
  6. self.function_map = dict()
  7. Namespace.__instance = self
  8. else:
  9. raise Exception("cannot instantiate a virtual Namespace again")
  10. @staticmethod
  11. def get_instance():
  12. if Namespace.__instance is None:
  13. Namespace()
  14. return Namespace.__instance
  15. def register(self, fn):
  16. """在虚拟的命名空间中注册函数,并返回Function类的可调用实例"""
  17. func = Function(fn)
  18. self.function_map[func.key()] = fn
  19. return func

Namespace类有一个register方法,该方法将函数 fn 作为参数,为其创建一个唯一的键,并将函数存储在字典中,最后返回封装了 fn 的Function的实例。这意味着 register 函数的返回值也是可调用的,并且(到目前为止)它的行为与被封装的函数 fn 完全相同。

  1. def area(l, b):
  2. return l * b
  3. >>> namespace = Namespace.get_instance()
  4. >>> func = namespace.register(area)
  5. >>> func(3, 4)
  6. 12

使用装饰器作为钩子

既然已经定义了一个能够注册函数的虚拟命名空间,那么,我们还需要一个钩子来在函数定义期间调用它。在这里,我们会使用 Python 装饰器。

在 Python 中,装饰器用于封装一个函数,并允许我们在不修改该函数的结构的情况下,向其添加新功能。装饰器把被装饰的函数 fn 作为参数,并返回一个新的函数,用于实际的调用。新的函数会接收原始函数的 args 和 kwargs,并返回最终的值。

以下是一个装饰器的示例,演示了如何给函数添加计时功能。

  1. import time
  2. def my_decorator(fn):
  3. """这是一个自定义的函数,可以装饰任何函数,并打印其执行过程的耗时"""
  4. def wrapper_function(*args, **kwargs):
  5. start_time = time.time()
  6. # 调用被装饰的函数,并获取其返回值
  7. value = fn(*args, **kwargs)
  8. print("the function execution took:", time.time() - start_time, "seconds")
  9. # 返回被装饰的函数的调用结果
  10. return value
  11. return wrapper_function
  12. @my_decorator
  13. def area(l, b):
  14. return l * b
  15. >>> area(3, 4)
  16. the function execution took: 9.5367431640625e-07 seconds
  17. 12

在上面的例子中,我们定义了一个名为 my_decorator 的装饰器,它封装了函数 area,并在标准输出上打印出执行 area 所需的时间。

每当解释器遇到一个函数定义时,就会调用装饰器函数 my_decorator(用它封装被装饰的函数,并将封装后的函数存储在 Python 的局部或全局命名空间中),对于我们来说,它是在虚拟命名空间中注册函数的理想钩子。

因此,我们创建了名为overload的装饰器,它能在虚拟命名空间中注册函数,并返回一个可调用对象。

  1. def overload(fn):
  2. """用于封装函数,并返回Function类的一个可调用对象"""
  3. return Namespace.get_instance().register(fn)

overload装饰器借助命名空间的 .register() 函数,返回 Function 的一个实例。现在,无论何时调用函数(被 overload 装饰的),它都会调用由 .register() 函数所返回的函数——Function 的一个实例,其 call 方法会在调用期间使用指定的 args 和 kwargs 执行。

现在剩下的就是在 Function 类中实现__call__方法,使得它能根据调用期间传入的参数而调用相应的函数。

从命名空间中找到正确的函数

想要区别出不同的函数,除了通常的模块、类和函数名以外,还可以依据函数的参数数量,因此,我们在虚拟的命名空间中定义了一个 get 方法,它会从 Python 的命名空间中读取待区分的函数以及实参,最后依据参数的不同,返回出正确的函数。我们没有更改 Python 的默认行为,因此在原生的命名空间中,同名的函数只有一个。

这个 get 函数决定了会调用函数的哪个实现(如果重载了的话)。找到正确的函数的过程非常简单——先使用 key 方法,它利用函数和参数来创建出唯一的键(正如注册时所做的那样),接着查找这个键是否存在于函数注册表中;如果存在,则获取其映射的实现。

  1. def get(self, fn, *args):
  2. """从虚拟命名空间中返回匹配到的函数,如果没找到匹配,则返回None"""
  3. func = Function(fn)
  4. return self.function_map.get(func.key(args=args))

get 函数创建了 Function 类的一个实例,这样就可以复用类的 key 函数来获得一个唯一的键,而不用再写创建键的逻辑。然后,这个键将用于从函数注册表中获取正确的函数。

实现函数的调用

前面说过,每次调用被 overload 装饰的函数时,都会调用 Function 类中的__call__方法。我们需要让__call__方法从命名空间的 get 函数中,获取出正确的函数,并调用之。

__call__方法的实现如下:

  1. def __call__(self, *args, **kwargs):
  2. """重写能让类的实例变可调用对象的__call__方法"""
  3. # 依据参数,从虚拟命名空间中获取将要调用的函数
  4. fn = Namespace.get_instance().get(self.fn, *args)
  5. if not fn:
  6. raise Exception("no matching function found.")
  7. # 调用被封装的函数,并返回调用的结果
  8. return fn(*args, **kwargs)

该方法从虚拟命名空间中获取正确的函数,如果没有找到任何函数,它就抛出一个 Exception,如果找到了,就会调用该函数,并返回调用的结果。

运用函数重载

准备好所有代码后,我们定义了两个名为 area 的函数:一个计算矩形的面积,另一个计算圆的面积。下面定义了两个函数,并使用overload装饰器进行装饰。

  1. @overload
  2. def area(l, b):
  3. return l * b
  4. @overload
  5. def area(r):
  6. import math
  7. return math.pi * r ** 2
  8. >>> area(3, 4)
  9. 12
  10. >>> area(7)
  11. 153.93804002589985

当我们用一个参数调用 area 时,它返回了一个圆的面积,当我们传递两个参数时,它会调用计算矩形面积的函数,从而实现了函数 area 的重载。

原作者注:从 Python 3.4 开始,Python 的 functools.singledispatch 支持函数重载。从 Python 3.8 开始,functools.singledispatchmethod 支持重载类和实例方法。感谢 Harry Percival 的指正。

总结

Python 不支持函数重载,但是通过使用它的基本结构,我们捣鼓了一个解决方案。

我们使用装饰器和虚拟的命名空间来重载函数,并使用参数的数量作为区别函数的因素。我们还可以根据参数的类型(在装饰器中定义)来区别函数——即重载那些参数数量相同但参数类型不同的函数。

重载能做到什么程度,这仅仅受限于getfullargspec函数和我们的想象。使用前文的思路,你可能会实现出一个更整洁、更干净、更高效的方法,所以,请尝试实现一下吧。

正文到此结束。以下附上完整的代码:

  1. # 模块:overload.py
  2. from inspect import getfullargspec
  3. class Function(object):
  4.   """Function is a wrap over standard python function
  5.   An instance of this Function class is also callable
  6.   just like the python function that it wrapped.
  7.   When the instance is "called" like a function it fetches
  8.   the function to be invoked from the virtual namespace and then
  9.   invokes the same.
  10.   """
  11.   def __init__(self, fn):
  12.     self.fn = fn
  13.   
  14.   def __call__(self, *args, **kwargs):
  15.     """Overriding the __call__ function which makes the
  16.     instance callable.
  17.     """
  18.     # fetching the function to be invoked from the virtual namespace
  19.     # through the arguments.
  20.     fn = Namespace.get_instance().get(self.fn, *args)
  21.     if not fn:
  22.       raise Exception("no matching function found.")
  23.     # invoking the wrapped function and returning the value.
  24.     return fn(*args, **kwargs)
  25.   def key(self, args=None):
  26.     """Returns the key that will uniquely identifies
  27.     a function (even when it is overloaded).
  28.     """
  29.     if args is None:
  30.       args = getfullargspec(self.fn).args
  31.     return tuple([
  32.       self.fn.__module__,
  33.       self.fn.__class__,
  34.       self.fn.__name__,
  35.       len(args or []),
  36.     ])
  37. class Namespace(object):
  38.   """Namespace is the singleton class that is responsible
  39.   for holding all the functions.
  40.   """
  41.   __instance = None
  42.   def __init__(self):
  43.     if self.__instance is None:
  44.       self.function_map = dict()
  45.       Namespace.__instance = self
  46.     else:
  47.       raise Exception("cannot instantiate Namespace again.")
  48.   @staticmethod
  49.   def get_instance():
  50.     if Namespace.__instance is None:
  51.       Namespace()
  52.     return Namespace.__instance
  53.   def register(self, fn):
  54.     """registers the function in the virtual namespace and returns
  55.     an instance of callable Function that wraps the function fn.
  56.     """
  57.     func = Function(fn)
  58.     specs = getfullargspec(fn)
  59.     self.function_map[func.key()] = fn
  60.     return func
  61.   
  62.   def get(self, fn, *args):
  63.     """get returns the matching function from the virtual namespace.
  64.     return None if it did not fund any matching function.
  65.     """
  66.     func = Function(fn)
  67.     return self.function_map.get(func.key(args=args))
  68. def overload(fn):
  69.   """overload is the decorator that wraps the function
  70.   and returns a callable object of type Function.
  71.   """
  72.   return Namespace.get_instance().register(fn)

最后,演示代码如下:

  1. from overload import overload
  2. @overload
  3. def area(length, breadth):
  4.   return length * breadth
  5. @overload
  6. def area(radius):
  7.   import math
  8.   return math.pi * radius ** 2
  9. @overload
  10. def area(length, breadth, height):
  11.   return 2 * (length * breadth + breadth * height + height * length)
  12. @overload
  13. def volume(length, breadth, height):
  14.   return length * breadth * height
  15. @overload
  16. def area(length, breadth, height):
  17.   return length + breadth + height
  18. @overload
  19. def area():
  20.   return 0
  21. print(f"area of cuboid with dimension (4, 3, 6) is: {area(4, 3, 6)}")
  22. print(f"area of rectangle with dimension (7, 2) is: {area(7, 2)}")
  23. print(f"area of circle with radius 7 is: {area(7)}")
  24. print(f"area of nothing is: {area()}")
  25. print(f"volume of cuboid with dimension (4, 3, 6) is: {volume(4, 3, 6)}")

为什么 Python 没有函数重载?如何用装饰器实现函数重载?的更多相关文章

  1. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

     1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题 ...

  2. Python【第四课】 装饰器

    本篇内容 什么是装饰器 装饰器需要遵循的原则 实现装饰器的知识储备 高阶函数 函数嵌套 闭包函数 无参函数 装饰器示例 1.什么是装饰器 器即函数 装饰即修饰,意指为其他函数添加新功能 装饰器定义:本 ...

  3. day20_函数的闭包 与 装饰器

    #!/usr/bin/env python # -*- coding:utf-8 -*- # # 一些文章 # https://www.cnblogs.com/Vae1242/p/6944338.ht ...

  4. Python的函数式编程-传入函数、排序算法、函数作为返回值、匿名函数、偏函数、装饰器

    函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计的基本单元. ...

  5. Python【第四篇】函数、内置函数、递归、装饰器、生成器和迭代器

    一.函数 函数是指将一组语句的集合通过一个名字(函数名)封装起来,要想执行这个函数,只需调用其函数名即可 特性: 减少重复代码 使程序变的可扩展 使程序变得易维护 1.定义 def 函数名(参数): ...

  6. Python 带参数的装饰器 [2] 函数参数类型检查

    在Python中,不知道函数参数类型是一个很正常的事情,特别是在一个大项目里.我见过有些项目里,每一个函数体的前十几行都在检查参数类型,这实在是太麻烦了.而且一旦参数有改动,这部分也需要改动.下面我们 ...

  7. Day11 Python基础之装饰器(高级函数)(九)

    在python中,装饰器.生成器和迭代器是特别重要的高级函数   https://www.cnblogs.com/yuanchenqi/articles/5830025.html 装饰器 1.如果说装 ...

  8. python 函数名 、闭包 装饰器 day13

    1,函数名的使用. 函数名是函数的名字,本质就是变量,特殊的变量.函数名()加括号就是执行此函数. 1,单独打印函数名就是此函数的内存地址. def func1(): print(555) print ...

  9. python 全栈开发,Day11(函数名应用,闭包,装饰器初识,带参数以及带返回值的装饰器)

    一.函数名应用 函数名是什么?函数名是函数的名字,本质:变量,特殊的变量. 函数名(),执行此函数. python 规范写法 1. #后面加一个空格,再写内容,就没有波浪线了. 2.一行代码写完,下面 ...

随机推荐

  1. 从 demo 到生产 - 手把手写出实战需求的 Flink 广播程序

    Flink 广播变量在实时处理程序中扮演着很重要的角色,适当的使用广播变量会大大提升程序处理效率. 本文从简单的 demo 场景出发,引入生产中实际的需求并提出思路与部分示例代码,应对一般需求应该没有 ...

  2. 【海通国际】Joe Lowry(Mr. Lithium)谈全球电池原材料供应危机

    [海通国际]Joe Lowry(Mr. Lithium)谈全球电池原材料供应危机 环球锂业公司(Global Lithium)总裁Joe Lowry日前接受了欧洲锰业Euro Manganese的邀请 ...

  3. centos下查看网卡,主板,CPU,显卡,硬盘型号等硬件信息

    centos下查看网卡,主板,CPU,显卡,硬盘型号等硬件信息 rose_willow rose_willow 发布于 2016/06/16 11:32 字数 902 阅读 405 收藏 0 点赞 0 ...

  4. Python实现TCP通讯

    Environment Client:Windows Server:KaLi Linux(VM_virtul) Network:Same LAN Client #!/usr/bin/python3 # ...

  5. 001.Ansible简介与基本安装

    一 自动化运维 1.1 运维的自动化发展历程 IAAS:infratructure as a Service PAAS:platform as a Service 平台及服务 SAAS:softwar ...

  6. linux 磁盘管理命令之df-(转自 Howie的专栏)

    linux中df命令的功能是用来检查linux服务器的文件系统的磁盘空间占用情况.可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息. 1.命令格式: df [选项] [文件] 2.命 ...

  7. Jmeter- 笔记7 - 服务器监控(ServerAgent配置)

    文件:ServerAgent - 2.2.3.zip  放网盘了 在服务器的操作:只需要把这个文件上传到被监控服务器,然后解压,启动sh startagent.sh --udp-port 0 --tc ...

  8. lms框架模块详解

    模块的定义 一般地,开发者如果想要在一个自定义的程序集(包)中注册相关的服务,或者在应用初始化或停止时执行一段自定义的代码,那么您可能需要将该程序集(包)定义为一个模块. lms框架存在两种类型的模块 ...

  9. 情景剧:C/C++中的未定义行为(undefined behavior)

    写在前面 本文尝试以情景剧的方式,轻松.直观地解释C/C++中未定义行为(undefined behavior)的概念.设计动机.优缺点等内容1,希望读者能够通过阅读本文,对undefined beh ...

  10. FPGA多功能应用处理器

    FPGA多功能应用处理器 编解码加速卡 概述: 对于H.265/HEVC/VP9编解码处理,FPGA编解码加速卡方案有着完善的功能和preset配置,支持最多的有利于提高画质和降低bitrate的功能 ...