在python里是没有所谓的函数重载的。函数是一个可调用对象的实例,其本身就是一个变量。在这种情况下,定义一个同名函数,效果会像我们给变量赋新的值一样,覆盖其原先的值。
一般情况下,我们是通过参数默认值等方法实现一部分的函数重载方法的。这固然在一定程度上实现了函数重载的效果,然而我们还是会在某些情况下需要更一般的重载。
考虑这样一个场景,传入的参数可能是int,float,str,以及其它类型。
- 对于int类型,返回它的平方
- 对于float类型,返回它的立方
- 对于str类型,返回它的长度
- 对于其他类型,抛出一个异常。
一个最简单的实现想法便是通过多个 if,else 来实现。
方法一:使用条件分支
代码如下
def fun(obj):
if type(obj) == int:
return obj * obj
elif type(obj) == float:
return obj ** 3
elif type(obj) == str:
return len(obj)
else:
raise ValueError("传入错误参数")
if __name__ == '__main__':
assert fun(2) == 4
assert fun(2.5) == 15.625
assert fun("2") == 1
try:
fun([])
except ValueError:
print("运行正确")
运行效果
运行正确
每多一个需要按类型调用的需求,我们就需要多一行 if-else。
我们用的毕竟是Python,很幸运的,Python的标准库为我们提供了一个函数装饰器:singledispatch 单分派泛函数装饰器。
方法二:singledispatch 装饰器
@singledispatch
def fun(obj):
raise ValueError("传入错误参数")
@fun.register
def _(num: int):
return num * num
@fun.register
def _(num: float):
return num ** 3
@fun.register
def _(text: str):
return len(text)
被singledispatch 装饰的函数会作为兜底情况或者说默认情况。而被register装饰的函数,会把参数类型注解作为分派条件。
当我们传入参数时,被装饰过的函数便根据传入的参数的类型信息,调用我们相应定义的函数了。
我们使用先前条件分支实现的一样的测试代码,可以得到一样的测试结果。
更进一步地,多分派泛函数该如何处理?
我们可能会有单分派的需求,同样地,根据多个值完成不同的操作,也是潜在的可能的需求。我们该如何实现呢?
回过头来看单分派函数,实际上它是一种策略模式的体现。借鉴这种想法,我们也可以实现一个类似装饰器。
我们考虑以所有形参对应的类型进行分派。
具体的做法是,把各函数的参数的类型组成元组作为键,值则为相应的函数,储存在字典中,当函数被调用是,根据入参的类型执行相应函数。于是我们可以写出如下代码:
def dispatch(func):
method = {}
default = func
def register(func):
key = tuple((value for key, value in func. __annotations__ .items() if key != "return"))
method[key] = func
return func
@functools.wraps(func)
def dispatch_wrapper(*args):
key = tuple(type(i) for i in args)
try:
return method[key](*args)
except KeyError:
return default(*args)
dispatch_wrapper.register = register
return dispatch_wrapper
测试
@dispatch
def foo(*args):
raise ValueError
@foo.register
def _(a: int, b: int):
return a + b
@foo.register
def _(a: int, b: float, c: int):
return a + b + c
运行结果
3
6.0
Traceback (most recent call last):
......
raise ValueError
ValueError
可以看到,上述的代码已经完成了我们最低层度的功能要求了。当然实际上这个代码还有很多bug可以修复,不过这都是后话。
在Python中,函数是所谓的一等对象,而正是由于这个特性,Python特地提供了装饰器的语法糖。借助装饰器,我们可以比较轻松地实现策略模式。
若不局限于本文假设的需求,可以看到的是,在我们常用的一些web框架上,例如 Flask 和 FastAPI ,它们的路由也同样是一个基于装饰器的策略模式。
注:本文纯属作者个人见解,如有误解或错误,欢迎批评指正。