zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    STL"源码"剖析
    Excel如何进行SVN
    BootstrapTable+KnockoutJS
    WebApi 集成 Swagger
    Java Core和HeapDump
    深入分析 Java 中的中文编码问题(1)
    Java编码浅析(注意区分三个概念)(转)
    理解并解决GBK转UTF-8奇数中文乱码(转)
    Java实现敏感词过滤(转)
    Android MediaPlayer状态机
  • 原文地址:https://www.cnblogs.com/zhuangliu/p/10851268.html
Copyright © 2011-2022 走看看