Python中singledispatch装饰器实现函数重载

本文参照"流畅的Python"这本书有关于singledispatch实现函数重载的阐述[1].

假设我们现在要实现一个函数, 功能是将一个对象转换成html格式的字符串. 怎么重载呢?

你可能会想, 用什么装饰器, Python不是动态类型么, 写成如下这样不就重载了嘛? 

def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

这个函数可以接受任意类型的参数. 你看,这不就重载了么?

如果我想让不同类型的对象有不同形式的html字符串呢 ? 你可能会说, 那就加个类型判断呗! 像下面这样.

def ifelseHtmlize(obj):
    if isinstance(obj, str):
        content = html.escape(repr(obj))
        content = '<pre>{}</pre>'.format(content)
    elif isinstance(obj, numbers.Integral):
        content = "<pre>{0} (0x{0:x})</pre>".format(obj)
    
    return content

额...这样当然可以实现上述需求. 然而, 当每种类型的处理逻辑比较复杂时, 以上方法大大增加了函数的篇幅, 影响可读性和可维护性. 你可能又会说, 那把每个分支抽象成函数就好了, 这样ifelseHtmlize函数的代码量就少了. 但是这样需要抽象出很多名字不一样的函数, 维护起来还是不太容易. 

因此, 我们需要寻求一种方法, 让每个重载函数能够关注自身需要处理的类型. 而且可以很简单的添加和去除. 于是就有了singledispatch这个装饰器. 先简单介绍下装饰器, 装饰器本质上是个函数, 其输入是一个函数, 返回值也是个函数. 把"@装饰器"放到哪个函数的头顶, 哪个函数就会作为参数输入到装饰器, 返回的函数再赋值给被装饰的函数. 这个技术的目的是对被装饰的函数做一些其他的手脚, 使其具有一些需要的特性. 例如singledispatch是Python内置众多装饰器之一, 就是为了达到函数重载的目的.

如何用singledispatch实现重载呢, 直接上代码:

@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace("
", "<br>
")
    return '<pre>{}</pre>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return "<pre>{0} (0x{0:x})</pre>".format(n)

乍一看脑壳疼. 简化一下, 如果去掉函数头顶上的装饰器, 再将这三个函数看作是一个函数名, 这和C++里的重载是类似的. 什么? 没学过C++, 那这句话当我没说, 接着看.

首先看第一个函数, 之前实现的htmlize函数被singledispatch装饰了, 意味着htmlize再也不是原来那个单纯的htmlize了. 现在的htmlize函数, 是被传入singledispatch后再返回出来的那个对象/函数. 具体的我们看一下singledispatch对htmlize都干了什么.

def singledispatch(func):
    # 用来记录 类型->函数
    registry = {}
    ...
    
    # 用来获得 指定类型的重载函数
    def dispatch(cls):
    ...
    
    # 注册新的 类型->函数
    def register(cls, func=None):
    ...
    
    # 重载函数入口
    def wrapper(*args, **kw)
    ...
    
    # 默认的重载函数
    registry[object] = func
    wrapper.register = register
    wrapper.dispatch = dispatch
    ...

    # 返回 wrapper供使用这调用重载函数
    return wrapper    

在singledispatch函数中, 参数func就是原始的htmlize函数. 然后先定义了一个字典registry, 看这个名字大概能猜出这是干嘛的. 对了, 就是注册用的, 注册啥呢? 当然是新的重载函数. 接着我们跳过中间几个函数, 先看下面一句registry[object] = func, 这将object类型对应的重载函数设置为func, 也就是传入的htmlize. 为什么是object呢? 这里埋个伏笔, 后面再讲. 现在说一下跳过的那三个函数, 实际上是三个闭包:

dispatch: 输入参数cls是类型, 根据给定的cls去registry中找对应的重载函数, 然后返回给调用者. 后面会见到这个函数是如何被调用的.

register: 用来注册新的重载函数. 核心的功能就是向registry中添加新的 类型->函数.

wrapper: 重载函数调用的入口, 负责执行正确的重载函数, 并返回结果.

最后对wrapper函数赋值了两个属性register和dispatch, 分别是同名的两个闭包, 接着返回wrapper成为新的htmlize. 什么, 没听说过直接给函数属性赋值? 看PEP 232 -- Function Attributes. 这里重点关注一下register, 有了它, 用户可以通过wrapper调用这个闭包. 意味着用户可以用register注册新的重载函数. 我们最终的目的要呼之欲出啦!

到这里只是对htmlize函数进行重载的使能. 接下来就可以定义htmlize的重载函数了. 直接上代码:

@htmlize.register(str)
def htmlize4str(text):
    content = html.escape(text).replace("
", "<br>
")
    return '<pre>{}</pre>'.format(content)

@htmlize.register(numbers.Integral)
def htmlize4Integral(n):
    return "<pre>{0} (0x{0:x})</pre>".format(n)

上面两个函数就是htmlize的两个重载函数, 分别用于处理字符串和Integral类型. 这两个函数均被htmlize.register(cls)返回的函数装饰. 我们知道, 上述singledispatch返回的wrapper会重新赋值给htmlize, 所以调用htmlize.register(cls)即是调用闭包register. 我们以htmlize.register(str)为例, 看看闭包register干了什么:

1     def register(cls, func=None):
2         ...
3         if func is None:
4             return lambda f: register(cls, f)
5         registry[cls] = func
6         ...
7         return func

调用register(str), 因为func是None, 所以进入分支, 直接返回一个函数lambda f: register(str, f). 返回之后的函数作为装饰器, 对htmlize4str函数进行装饰. 于是htmlize4str函数作为lambda表达式中的f, 实际上调用了register(str, htmlize4str). 于是, 又回到了上述函数, 这次func==htmlize4str非None, 于是str->htmlize4str得以注册, 最后返回htmlize4str.

以上, 就完成了注册重载函数的过程了. 那如何实现传入htmlize不同参数, 执行不同的函数呢. 比如调用htmlize("23333"), 如何定位到htmlize4str("23333")呢? 现在回忆一下singledispatch装饰的htmlize现在是什么? 是wrapper闭包啊, 所以调用htmlize("23333"), 即调用wrapper("23333"). 我们看看wrapper做了什么:

1     def wrapper(*args, **kw):
2         return dispatch(args[0].__class__)(*args, **kw)

wrapper将输入的第一个参数的__class__, 即类型输入到dispatch. 我们之前提到过, dispatch这个函数用于找到指定类型的重载函数. dispatch返回后执行这个重载函数, 再将结果返回给调用者. 例如, wrapper("23333")首先调用dispatch(str), 因为"23333"的类型是str, 找到对应的重载函数, 即htmlize4str, 然后再调用htmlize4str("23333"). 实现重载啦啦啦! 而且我们发现重载函数的函数名, 对于调用htmlize是透明的, 根本用不到. 所以重载的函数名可以用_替代, 这样更好维护代码.

最后我们看看dispatch这个闭包干了些什么:

 1     def dispatch(cls):
 2         """generic_func.dispatch(cls) -> <function implementation>
 3 
 4         Runs the dispatch algorithm to return the best available implementation
 5         for the given *cls* registered on *generic_func*.
 6 
 7         """
 8         ...
 9             try:
10                 impl = registry[cls]
11             except KeyError:
12                 impl = _find_impl(cls, registry)
13         ...
14         return impl

核心的功能就是到registry里找对应的cls类型的重载函数, 然后返回就行了. 那如果没找到呢? 比如我调用了htmlize({1, 2, 3}), 这时cls是list类型, 在registry里没有找到对应的重载函数咋办呢? 在上述代码中, 捕捉了registry抛出的KeyError异常, 即在没有找到时执行_find_impl(cls, registry), 这又是干嘛的呢? 这里不展开讲了, 我也展不开. 总之, 用一句话来说: 找到registry中和cls类型最匹配的类型, 然后返回其重载函数.

看看现在我们的registry里有哪些类型呢? str, numbers.Integral. 哦!!! 还有object (回忆一下, 在singledispatch中有这么一句: registry[object] = func). Python里所有类型都继承object类型, 于是返回registry中object对应的重载函数, 即最原始的htmlize. 

以上.

[1] Ramalho, Luciano. Fluent Python : clear, concise, and effective programming. Sebastopol, CA : O'Reilly, 2015.

原文地址:https://www.cnblogs.com/zhuangliu/p/10851268.html