本文参照"流畅的Python"这本书有关于singledispatch实现函数重载的阐述[1].
假设我们现在要实现一个函数, 功能是将一个对象转换成html格式的字符串. 怎么重载呢?
你可能会想, 用什么装饰器, Python不是动态类型么, 写成如下这样不就重载了嘛?
def htmlize(obj):
content = html.escape(repr(obj))
return '
{}'.format(content)
这个函数可以接受任意类型的参数. 你看,这不就重载了么?
如果我想让不同类型的对象有不同形式的html字符串呢 ? 你可能会说, 那就加个类型判断呗! 像下面这样.
def ifelseHtmlize(obj):
if isinstance(obj, str):
content = html.escape(repr(obj))
content = '
{}'.format(content)
elif isinstance(obj, numbers.Integral):
content = "
{0} (0x{0:x})".format(obj)
return content
额...这样当然可以实现上述需求. 然而, 当每种类型的处理逻辑比较复杂时, 以上方法大大增加了函数的篇幅, 影响可读性和可维护性. 你可能又会说, 那把每个分支抽象成函数就好了, 这样ifelseHtmlize函数的代码量就少了. 但是这样需要抽象出很多名字不一样的函数, 维护起来还是不太容易.
因此, 我们需要寻求一种方法, 让每个重载函数能够关注自身需要处理的类型. 而且可以很简单的添加和去除. 于是就有了singledispatch这个装饰器. 先简单介绍下装饰器, 装饰器本质上是个函数, 其输入是一个函数, 返回值也是个函数. 把"@装饰器"放到哪个函数的头顶, 哪个函数就会作为参数输入到装饰器, 返回的函数再赋值给被装饰的函数. 这个技术的目的是对被装饰的函数做一些其他的手脚, 使其具有一些需要的特性. 例如singledispatch是Python内置众多装饰器之一, 就是为了达到函数重载的目的.
如何用singledispatch实现重载呢, 直接上代码:
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '
{}'.format(content)
@htmlize.register(str)
def _(text):
content = html.escape(text).replace("\n", "
\n")
return '
{}'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return "
{0} (0x{0:x})".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("\n", "
\n")
return '
{}'.format(content)
@htmlize.register(numbers.Integral)
def htmlize4Integral(n):
return "
{0} (0x{0:x})".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) ->
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.
标签:函数,Python,register,htmlize,singledispatch,registry,重载,cls
来源: https://www.cnblogs.com/zhuangliu/p/10851268.html