Python 复用装饰器代码

前几天同事问我一个问题:Python 代码中,两个函数装饰器部分的代码太多了,而且有很多重复的,能否复用?这个问题我一开始也没完全听明白需求是啥,不过看了他的代码就明白了。

这里,我将他的代码简化如下:

这里,hello_woldhello_world2 的装饰器部分几乎相同,唯一不同的部分是 @add_args("bar", "bar1") 的第二个参数不同。所以他想要服用装饰器部分的代码。想要达到的效果如下面这个写法,希望能和上面的代码完全等同。

这个需求是用 click 这个库定义子命令的时候,子命令之间有很多重复的。在 golang 中,使用 cobra 库可以将一部分参数都抽象出来,复用这部分代码。在 Python 中,click 库看起来不太容易做到这一点。我觉得也许可以抽象出来命令 Group 来解决这个问题,但是同事听了直摇头,觉得 command sub1 sub2 这种敲下去两层子命令不太好,一层子命令是能接受的极限了。

有没有一种方法,能够复用这部分重复的代码,还不影响命令的 UI(怎么,Cli 也是一种 UI!)

回来试了一下,发现是完全可以做到的。

如果了解装饰器基础知识,可以直接跳到文末看答案。上面没有列出源代码的四个装饰器,源代码如下:

Python 中的函数是一等公民

这句话的意思是,Python 中的函数和其他变量一样,可以被创建,修改,赋值,可以作为参数。

下面是一个 decorating_bark 函数,这里面什么也没有做,传进来一个函数,拿到一个函数,只是为了证明函数可以作为一个参数一样传递和返回。

输出结果是:

当然,也可以做点什么,这段代码中的 bark 拿到函数之后,返回了一个新的函数,新的函数先是 print 了一下,然后调用原来的函数:

注意在 cat_say = bark(cat_say) 这一行,bark 所返回的新的函数赋值给了原来的 cat_say。运行的结果如下:

两行内容都是打印在 cat_say() 调用的时候发生了,说明它的行为被 bark(cat)的返回值给替换了。

这两个函数 decorating_barkbark 就是上面的装饰器,但是这一段内容中我们没提起过装饰器,都是在讲函数。

Python 中装饰器是什么?

cat_say = bark(cat_say) 这一行,我们也可以这么写:@bark。但是这一行必须要写在 def 的上方:

这就是装饰器了。所以,装饰器只是一个语法糖。它没有给 Python 添加新的功能,只是让代码看起来更漂亮简洁了一些。

可以传入参数的装饰器

下面这个例子复杂了一些:

但是我们只用上面学过的内容,不需要任何新的知识,就可以理解它。

you_decide_what_to_say("oh!") 只是一个函数调用,我们将 @ 这个语法糖去掉,就变成了下面这样:

看起来还是有一些复杂,我这么写,就简单了:

对照最初的语法糖,可以看到所谓“带有参数的装饰器”,本质其实就是一个函数调用,这个函数调用会返回一个函数,返回的函数才是装饰器,用来装饰 def 的函数。

换句话说,“带有参数的装饰器” 的本质是一个装饰器制造器(decorator maker,我发明的叫法)。

由于这个例子中,实际的装饰器什么也没做,所以看起来还相对简单。有了这些知识,我们可以来看最后一个装饰器,它比上一个增加的内容,就是对函数本身做了修改。

其中,add_args 是一个制造装饰器的函数,function_wrapper 是它制造出来的装饰器,real_func 是真正会返回的函数,会去替换原来的函数,它的内部调用了原来的函数,不过调用之前,它先修改了入参。

我还专门画了一个直观的图:

Python 带有参数的装饰器分解

答案

到这里,可以发现装饰器的代码也是可以复用的,因为我们可以将其当作函数来调用:

注意,包装的顺序很重要,因为装饰器是有顺序的,最里面的会先执行,最外面的后执行。读者可以复制到前面的代码中,会发现输出完全一样。这样做我们只是删除了语法糖,只使用了最原始的函数。(其实,代码中没有地方在定义装饰器,而只是在定义函数!装饰器是函数调用的语法糖)

但是,我们是否可以继续使用语法糖来复用这部分代码呢?答案是可以的。

因为 @ 必须在 def 的上方使用,所以我们必须要有 def 才行。那要 def 什么呢?我们只是想组合起来已有的装饰器,并不想改变原来的函数的行为。那就随便 def 一个新函数好了,只是新函数的内部啥也不用做,原原本本将原来的函数返回即可。

最后,上文中的 general_decorator 的实现可以如下:

读者若有兴趣,可以看下之前写过的另一篇有关装饰器的内容:Python装饰器兼容加括号与不加括号的写法。之后,相信如果看到装饰器的代码,就可以信心满满地说:“哈,我知道,这只不过是函数而已!”

不过,装饰器切不可滥用。一般定义装饰器的场景是制作框架,比如像 Flask 这种 web 框架,或者 Celery 这种异步框架。框架的制作者将装饰器定义好,用户就可以使用这些装饰器,好处是,用户看起来是在写普普通通的函数,但是确能通过装饰器告诉框架一些额外的信息,和框架配合工作地很好。

作为一个语法糖,装饰器可以很好的标记出来函数一些特殊的属性。它的目的是提高代码的可读性。可惜的是,笔者遇到过很多使用装饰器的代码,解决的问题确是普通的显式函数调用就可以完成的,使用装饰器反而让代码看起来更加复杂,语义上也说不过去,降低了代码的可读性。

什么时候该用函数调用,什么时候该用装饰器?这其实是需要在朝着写好代码的漫漫长路上不断练习的。但是我有一个捷径:如果你要标记这个函数的属性,比如标记它是异步任务,标记它失败需要自动重试(@retry),标记它和某一个 @api_route 关联,那么几句设计成装饰器;如果这个是函数本身的逻辑,比如需要先干这个再干那个,这三个函数都需要先干这个再干其他的。“这个”就是显式函数调用的场景。



Leave a comment

您的电子邮箱地址不会被公开。 必填项已用*标注