实现一个简单的多分派泛函数:Python中的策略模式| 8月更文挑战

865 阅读3分钟

在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 ,它们的路由也同样是一个基于装饰器的策略模式。

注:本文纯属作者个人见解,如有误解或错误,欢迎批评指正。