Python如何实现重载函数
2020-02-26
作者:Arpit
翻译:老齐
重载函数,即多个函数具有相同的名称,但功能不同。例如一个重载函数fn
,调用它的时候,要根据传给函数的参数判断调用哪个函数,并且执行相应的功能。
1 | int area(int length, int breadth) { |
上例是用C++写的代码,函数area
就是有两个不同功能的重载函数,一个是根据参数length和breadth计算矩形的面积,另一个是根据参数radius(圆的半径)计算圆的面积。如果用area(7)
的方式调用函数area
,就会实现第二个函数功能,当area(3, 4)
时调用的是第一个函数。
为什么Python中没有重载函数
Python中本没有重载函数,如果我们在同一命名空间中定义的多个函数是同名的,最后一个将覆盖前面的各函数,也就是函数的名称只能是唯一的。通过执行locals()
和globals()
两个函数,就能看到该命名空间中已经存在的函数。
1 | def area(radius): |
定义了一个函数之后,执行locals()
函数,返回了一个字典,其中是本地命名空间中所定义所有变量,键是变量,值则是它的引用。如果有另外一个同名函数,就会将本地命名空间的内容进行更新,不会有两个同名函数共存。所以,Python不支持重载函数,这是发明这个语言的设计理念,但是这并不能阻挡我们不能实现重载函数。下面就做一个试试。
在Python中实现重载函数
我们应该知道Python怎么管理命名空间,如果我们要实现重载函数,必须:
- 在稳定的虚拟命名空间管理所定义的函数
- 根据参数调用合适的函数
为了简化问题,我们将实现具有相同名称的重载函数,它们的区别就是参数的个数。
封装函数
创建一个名为Function
的类,并重写实现调用的__call__
方法,再写一个名为key
的方法,它会返回一个元组,这样让就使得此方法区别于其他方法。
1 | from inspect import getfullargspec |
在上面的代码片段中,key
方法返回了一个元组,其中的元素包括:
- 函数所属的模块
- 函数所属的类
- 函数名称
- 函数的参数长度
在重写的__call__
方法中调用作为参数的函数,并返回计算结果。这样,实例就如同函数一样调用,它的表现效果与作为参数的函数一样。
1 | def area(l, b): |
在上面的举例中,函数area
作为Function
实例化的参数,key()
返回的元组中,第一个元素是模块的名称__main__
,第二个是类<class 'function'>
,第三个是函数的名字area
,第四个则是此函数的参数个数2
。
从上面的示例中,还可以看出,调用实例func
的方式,就和调用area
函数一样,提供参数3
和4
,就返回12
,前面调用area(3, 4)
也是同样结果。这种方式,会在后面使用装饰器的时候很有用。
构建虚拟命名空间
我们所构建的虚拟命名空间,会保存所定义的所有函数。
1 | class Namespace(object): |
Namespace
类中的方法register
以函数fn
为参数,在此方法内,利用fn
创建了Function
类的实例,还将它作为字典的值。那么,方法register
的返回值,也是一个可调用对象,其功能与前面封装的fn
函数一样。
1 | def area(l, b): |
用装饰器做钩子
我们已经定义了一个虚拟命名空间,并且可以向其中注册一个函数,下面就需要一个钩子,在该函数生命周期内调用它,为此使用Python的装饰器。在Python中,装饰器是一种封装的函数,可以将它加到一个已有函数上,并不需要理解其内部结构。装饰器接受函数fn
作为参数,并且返回另外一个函数,在这个函数被调用的时候,可以用args
和kwargs
为参数,并得到返回值。
下面是一个简单的封装器示例:
1 | import time |
在上面的示例中,定义了名为my_decorator
的装饰器,并用它装饰函数area
,在交互模式中调用,打印出area(3,4)
的执行时间。
装饰器my_decorator
装饰了一个函数之后,当执行函数的时候,该装饰器函数也每次都要调用,所以,装饰器函数是一个理想的钩子,借助它可以向前述定义的虚拟命名空间中注册函数。下面创建一个名为overload
的装饰器,用它在虚拟命名空间注册函数,并返回一个可执行对象。
1 | def overload(fn): |
overload
装饰器返回Function
实例,作为.register()
的命名空间。现在,不论什么时候通过overload
调用函数,都会返回.register()
,即Function
实例,并且,在调用的时候,__call__
也会执行。
从命名空间中查看函数
除通常的模块类和名称外,消除歧义的范围是函数接受的参数数,因此我们在虚拟命名空间中定义了一个称为get
的方法,该方法接受Python命名空间中的函数(将是最后一个同名定义 - 因为我们没有更改 Python 命名空间的默认行为)和调用期间传递的参数(我们的非义化因子),并返回要调用的消除歧义函数。
此get
函数的作用是决定调用函数的实现(如果重载)。获取适合函数的过程非常简单,从函数和参数创建使用key
函数的唯一键(在注册时完成),并查看它是否存在于函数注册表中,如果在,就执行获取针对它存储操作。
1 | def get(self, fn, *args): |
在get
函数中创建了Function
的实例,它可以用key
方法得到唯一的键,并且不会在逻辑上重复,然后使用这个键在函数注册表中得到相应的函数。
调用函数
如上所述,每当被overload
装饰器装饰的函数被调用时,类Function
中的方法__call__
也被调用,从而通过命名空间的get
函数得到恰当的函数,实现重载函数功能。__call__
方法的实现如下:
1 | def __call__(self, *args, **kwargs): |
这个方法从虚拟命名空间中得到恰当的函数,如果它没有找到,则会发起异常。
重载函数实现
将上面的代码规整到一起,定义两个名字都是area
的函数,一个计算矩形面积,另一个计算圆的面积,两个函数均用装饰器overload
装饰。
1 | @overload |
当我们给调用的area
传一个参数时,返回圆的面积,两个参数时则计算了矩形面积,这样就实现了重载函数area
。
结论
Python不支持函数重载,但通过使用常规的语法,我们找到了它的解决方案。我们使用修饰器和用户维护的命名空间来重载函数,并使用参数数作为消除歧义因素。还可以使用参数的数据类型(在修饰中定义)来消除歧义—— 它允许具有相同参数数但不同类型的函数重载。重载的粒度只受函数getfullargspec
和我们的想象力的限制。更整洁、更简洁、更高效的方法也可用于上述构造。
原文链接:https://arpitbhayani.me/blogs/function-overloading
关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能