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.

  • 相关阅读:
    商贸通帐套隐藏方法
    固定资产打开提示:上年度数据未结转!
    ZOJ 2432 Greatest Common Increasing Subsequence
    POJ 1080 Human Gene Functions
    POJ 1088 滑雪
    POJ 1141 Brackets Sequence
    POJ 1050 To the Max
    HDOJ 1029 Ignatius and the Princess IV
    POJ 2247 Humble Numbers
    HDOJ 1181 变形课
  • 原文地址:https://www.cnblogs.com/zhuangliu/p/10851268.html
Copyright © 2011-2022 走看看