英文:https://arpitbhayani.me/blogs/function-overloading作者:arprit译者:豌豆花在猫下(《蟒猫》公众号作者)免责声明:本译文仅供交流学习.根据CCBY-NC-SA4.0获得许可。为便于阅读,内容略有修改。函数重载是指具有多个具有相同名称但具有不同签名或实现的函数。当调用重载函数fn时,程序会检查传递给函数的实际/形式参数,并相应地调用相应的实现。intarea(intlength,intbreadth){returnlength*breadth;}floatarea(intradius){return3.14*radius*radius;}在上面的例子中(用C++编写),函数area重载了两个实现。第一个函数有两个参数(都是整数),代表矩形的长和宽,返回矩形的面积。另一个函数只接受一个整数参数,代表圆的半径。当我们像area(7)这样调用函数区域时,它调用第二个函数,而area(3,4)调用第一个函数。为什么Python中没有函数重载?Python不支持函数重载。当我们定义多个同名函数时,后面的函数总是会覆盖前面的函数。因此,在一个命名空间中,每个函数名将只有一个条目。Python猫注:据说Python不支持函数重载,也就是说它没有使用语法糖。函数重载也可以在Python中使用functools库的singledispatch装饰器。原作者在文章末尾的注释中特别提到了这一点。我们可以通过调用locals()和globals()函数来查看Python的命名空间中有什么,这两个函数分别返回本地和全局命名空间。defarea(radius):return3.14*radius**2>>>locals(){...'area':,...}定义一个函数后,调用locals()函数,我们将看到它返回一个字典,其中包含在本地命名空间中定义的所有变量。字典的键是变量的名称,值是该变量的引用/值。程序运行时,如果遇到另一个同名函数,就会更新本地命名空间中的注册表项,从而消除两个函数共存的可能性。所以Python不支持函数重载。这是创建语言时做出的设计决定,但这并不妨碍我们实现它,所以让我们重载一些功能。在Python中实现函数重载我们已经知道Python是如何管理命名空间的。如果要实现函数重载,需要这样做:维护一个虚拟命名空间,在这个命名空间中根据每次传递的参数来管理函数定义,尽量调用合适的函数为了简单起见,当我们实现函数重载时,我们通过不同数量的参数来区分具有相同名称的函数。封装函数我们创建了一个名为Function的类,它可以封装任何函数,并通过重写的__call__方法调用该函数,还提供了一个名为key的方法,该方法返回一个tuple,使该函数在整个代码库中是唯一的。frominspectimportgetfullargspecclassFunction(object):"""函数类是标准Python函数的封装"""def__init__(self,fn):self.fn=fndef__call__(self,*args,**kwargs):"""像函数一样调用时,会调用封装好的函数,并返回函数的返回值。"""returnself.fn(*args,**kwargs)defkey(self,args=None):"""返回一个可以唯一标识一个函数的键(即使它被重载了)"""#如果没有指定args,如果args为None,则从函数定义中提取参数:args=getfullargspec(self.fn)。argsreturntuple([self.fn.__module__,self.fn.__class__,self.fn.__name__,len(argsor[]),])上面的代码片段中,key函数返回一个元组Group,这个元组唯一在代码库中标识函数,记录:函数所属的模块,函数所属的类函数名,函数接收的参数个数重写的__call__方法将调用封装的函数并返回计算值(这没什么特别的)。这允许Function的实例像函数一样被调用,并且它的行为与包装函数完全一样。defarea(l,b):returnl*b>>>func=Function(area)>>>func.key()('__main__',,'area',2)>>>func(3,4)12上面的例子中,函数area被封装在Function中,实例化为func。key()返回一个元组,第一个元素是模块名__main__,第二个是类,第三个是函数名区,第四个是函数接收的参数数量,为2.这个例子也说明我们可以像调用普通区域函数一样调用实例func。当传入参数3和4时,结果为12,也就是我们调用area(3,4)时得到的result。当我们接下来应用装饰器时,这种行为会派上用场。构建虚拟命名空间我们将创建一个虚拟命名空间来存储在定义阶段收集的所有函数。由于只有一个命名空间/注册表,我们创建一个单例类并将函数存储在字典中。这个字典的键不是函数名,而是我们从键函数中得到的元组,元组包含唯一标识一个函数的元素。通过这样做,我们可以将所有函数保存在注册表中,即使它们具有相同的名称(但参数不同),从而实现函数重载。classNamespace(object):"""Namespace是一个单例类,负责保存所有函数"""__instance=Nonedef__init__(self):ifself.__instanceisNone:self.function_map=dict()Namespace.__instance=selfelse:raiseException("cannotinstantiateavirtualNamespaceagain")@staticmethoddefget_instance():ifNamespace.__instanceisNone:Namespace()returnNamespace.__instancedefregister(self,fn):"""在虚拟寄存器中命名空间中的函数并返回Function类的可调用实例"""func=Function(fn)self.function_map[func.key()]=fnreturnfuncNamespace类有一个以函数fn作为参数的register方法,创建一个它的唯一键,将函数存储在字典中,最后返回一个封装了fn的Function实例。这意味着register函数的返回值也是可调用的,并且(到目前为止)它的行为与包装函数fn完全一样。defarea(l,b):returnl*b>>>namespace=Namespace.get_instance()>>>func=namespace.register(area)>>>func(3,4)12使用装饰器作为钩子因为已经有了定义了一个可以在其中注册函数的虚拟命名空间,我们还需要一个钩子来在函数定义期间调用它。在这里,我们将使用Python装饰器。在Python中,装饰器用于包装函数并允许我们在不修改函数结构的情况下向其添加新功能。装饰器将装饰函数fn作为参数,并为实际调用返回一个新函数。新函数采用原始函数的args和kwargs并返回最终值。下面是一个装饰器示例,演示了如何向函数添加计时功能。importtimedefmy_decorator(fn):"""这是一个自定义函数,可以装饰任何函数并打印其执行过程中花费的时间。"""defwrapper_function(*args,**kwargs):start_time=time.time()#调用装饰函数并获取其返回值value=fn(*args,**kwargs)print("函数执行时间:",time.time()-start_time,"seconds")#返回的是调用结果装饰函数返回值returnwrapper_function@my_decoratordefarea(l,b):returnl*b>>>area(3,4)函数执行耗时:9.5367431640625e-07seconds12在上面的例子中,我们定义了一个装饰器叫做my_decorator包装函数区域并在标准输出上打印执行区域所需的时间。每当解释器遇到函数定义时,就会调用装饰器函数my_decorator(用它来封装被装饰的函数,并将封装后的函数存储在Python的本地或全局命名空间中),对我们来说,是在虚拟命名空间中注册函数的理想钩子。因此,我们创建了一个名为overload的装饰器,它在虚拟命名空间中注册一个函数并返回一个可调用对象。defoverload(fn):"""用于封装函数,返回一个Function类的可调用对象"""returnNamespace.get_instance().register(fn)overload装饰器使用命名空间的.register()函数,返回函数的一个实例。现在,无论何时调用一个函数(装饰有overload),它都会调用.register()函数返回的函数——一个Function的实例,其调用方法将在调用期间使用指定的args和kwargs执行。现在剩下的就是实现Function类中的__call__方法,让它根据调用时传入的参数调用相应的函数。从命名空间中找到正确的函数为了区分不同的函数,除了通常的模块、类和函数名之外,还可以根据函数的参数个数,所以我们在虚命名空间中定义了一个get方法,它会从Python命名空间中读取需要区分的函数和实参,最后根据不同的参数返回正确的函数。我们没有更改Python的默认行为,因此在本机命名空间中,只有一个同名函数。get函数确定将调用函数的哪个实现(如果重载)。找到正确函数的过程非常简单——首先使用key方法,它使用函数和参数来创建一个唯一的键(就像注册时所做的那样),然后检查这个键是否存在于函数注册表中;如果是,则获取其映射的实现。defget(self,fn,*args):"""从虚拟命名空间返回匹配的函数,或者None"""func=Function(fn)returnself.function_map.get(func.key(args=args))get函数创建了Function类的一个实例,这样就可以复用该类的key函数来获取唯一的key,而无需编写创建key的逻辑。然后该密钥将用于从函数注册表中获取正确的函数。实现函数调用前面提到过,每次调用重载修饰的函数时,都会调用Function类中的__call__方法。我们需要让__call__方法从命名空间的get函数中获取正确的函数并调用它。__call__方法的实现如下:def__call__(self,*args,**kwargs):"""重写可以使类的实例成为可调用对象的__call__方法"""#根据参数,从虚拟命名空间获取要调用的函数fn=Namespace.get_instance().get(self.fn,*args)ifnotfn:raiseException("nomatchingfunctionfound.")#调用封装的函数并返回调用返回的结果fn(*args,**kwargs)此方法从虚拟命名空间获取正确的函数。如果未找到函数,则抛出异常。如果找到,则调用该函数并返回调用结果。.使用函数重载将所有代码就位后,我们定义了两个函数area:一个计算矩形的面积,另一个计算圆的面积。下面定义了两个函数,并用重载装饰器进行了装饰。@overloaddefarea(l,b):returnl*b@overloaddefarea(r):importmathreturnmath.pi*r**2>>>area(3,4)12>>>area(7)153.93804002589985当当我们用一个参数调用area时,它返回一个圆的面积,当我们传入两个参数时,它调用计算矩形面积的函数,从而实现了函数area的重载。原作者注:从Python3.4开始,Python的functools.singledispatch支持函数重载。从Python3.8开始,functools.singledispatchmethod支持重载类和实例方法。感谢HarryPercival的更正。总结Python不支持函数重载,但是通过使用它的基本结构,我们修补了一个解决方案。我们使用装饰器和虚拟命名空间来重载函数,并使用参数的数量作为区分函数的因素。我们还可以根据参数的类型(在装饰器中定义)来区分函数——即重载那些参数数量相同但参数类型不同的函数。可以实现重载的程度仅受getfullargspec函数和我们想象力的限制。使用前面的想法,你可能会得到一个更简洁、更干净、更高效的方法,所以请尝试实现它。正文到此结束。下面附上完整的代码:#模块:overload.pyfrominspectimportgetfullargspecclassFunction(object):"""Functionisawrapoverstandardpythonfunction此函数类的实例也可调用,就像它包装的python函数一样。当实例像函数一样被“调用”时,它会从虚拟命名空间中获取要调用的函数,然后调用该函数。"""def__init__(self,fn):self.fn=fndef__call__(self,*args,**kwargs):"""覆盖使实例可调用的__call__函数。"""#通过参数从虚拟命名空间获取要调用的函数。fn=Namespace.get_instance().get(self.fn,*args)ifnotfn:raiseException("nomatchingfunctionfound.")#调用包装函数并返回值。returnfn(*args,**kwargs)defkey(self,args=None):"""返回将唯一标识一个函数的键(即使它超载)。"""如果args为None:classNamespace(object):"""Namespace是负责保存所有功能的单例类。"""__instance=Nonedef__init__(self):如果self.__instance为None:self.function_map=dict()Namespace.__instance=selfelse:raiseException("无法再次实例化命名空间。")@staticmethoddefget_instance():ifNamespace.__instanceisNone:Namespace()returnNamespace.__instancedefregister(self,fn):“””在虚拟命名空间中注册函数并返回包装函数fn的可调用函数实例。"""func=Function(fn)specs=getfullargspec(fn)self.function_map[func.key()]=fnreturnfuncdefget(self,fn,*args):"""get从虚拟命名空间返回匹配函数。如果没有资助任何匹配函数,则返回None。"""func=Function(fn)returnself.function_map.get(func.key(args=args))defoverload(fn):"""overload是包装函数并返回函数类型的可调用对象的装饰器."""returnNamespace.get_instance().register(fn)最后,显示代码如下:fromoverloadimportoverload@overloaddefarea(length,breadth):returnlength*breadth@overloaddefarea(radius):importmathreturnmath.pi*radius**2@overloaddefarea(length,breadth,height):返回2*(length*breadth+breadth*height+height*length)@overloaddefvolume(length,breadth,height):返回length*breadth*height@overloaddefarea(length,breadth,height):returnlength+breadth+height@overloaddefarea():return0print(f"长方体的面积为(4??,3,6)是:{area(4,3,6)}")print(f"尺寸为(7,2)的矩形面积为:{面积(7,2)}")print(f"半径为7的圆的面积是:{area(7)}")print(f"空的区域是:{area()}")print(f"体积维度为(4,3,6)的长方体是:{volume(4,3,6)}"