python装饰器实现的函数重写,Python中singledispatch装饰器实现函数重载

本文参照"流畅的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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值