如何不使用 @,来调用 Python 装饰器

作者: 王炳明 分类: Python 高手进阶 发布时间: 2021-04-14 20:41 热度:94

1. Flask 的经典范例

若你有学习过 flask ,下面这个经典的入门示例,你一定非常熟悉

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

这些代码是什么意思呢?

  1. 首先导入了 Flask 类。 该类的实例将会成为我们的 WSGI 应用。
  2. 接着我们创建一个该类的实例。
  3. 然后使用 route() 装饰器来向 Flask 注册路由信息,该路由表示访问 / 的时候,会触发 hello_world 这个处理函数 。最后把消息返回。

在 Flask 刚出来的时候,使用装饰器来进行路由注册的用法,被很多人吹上了天,将其视作装饰器的经典使用范例。

再后来,有越来越多的框架,开始疯狂地借鉴这种用法,就比如我今天要讲的 werobot 这个框架。

2. 介绍下WeRobot

werobot 是一个专门用来微信公众号开发的框架,你们不知道也是正常的。

有人就要问了,写微信公众号不是有自己的后台吗?怎么还有专门的框架?

其实是这样的,当你注册一个微信公众号后,公众号的后台 Web 界面就已经给你提供了一套运营管理的套件,比如设置关键词回复。

可是官方默认的这组件,是静态的配置,无法为每个用户实现定制化的消息回复。

什么叫用户定制化的消息回复?

我举个例子,当你在我的后台回复 “老铁指数” 关键词的时候(赶紧去试试),可以查到你是什么时候关注我的,根据关键时间的长短,我会计算出专属于你的老铁指数。发送同一指令的用户却能接收到不同回复,这就叫做用户定制化,

而这个功能是在公众号的默认套件里,是无法实现这种定制化的动态回复,唯一的选择是:只能通过 werobot 自行开发。

如何不使用 @,来调用 Python 装饰器插图

出于测试的需要,我在 2 月份的时候,有取关过自己的公众号,因此老铁指数非常低,在看这篇文章的同学应该很多都比我高。

不过,在这里,我提醒下各位,千万别没事取关我的公众号,不仅你的号的老铁指数会清零,而且会被打上标记,回复关键字也无法获取任何帮助和资料啦,就像下面这位同学。

如何不使用 @,来调用 Python 装饰器插图(1)

这个功能其实我已经开发过很久了,但愿意付费解的人,却寥寥无几,可能互联网时代真的白嫖养成习惯了吧。

3. 关键词注册

上面扯远了,回到我们最开始的主题上来吧

根据 werobot 官方给出的使用示例,在 WeRobot 中注册一个关键词回复的功能,也是和 Flask 一样,使用的是装饰器。

下面是我使用的一个范例,你可以看看,@robot.filter('vip') 里的 vip 就是你在后台回复的关键字, response.vip 是我事先定义好的回复模板。

@robot.filter('vip')
def get_vip_code(message):
    user_info = client.get_user_info(message.source)
    save_data(user_info["nickname"], "回复", message.content)
    return response.vip

由于官方本身就推荐使用这种方式去注册关键词,因此你在网络上搜索的文章、博客全部都是基于这种写法编写的。

如何不使用 @,来调用 Python 装饰器插图(2)

不得不承认,使用装饰器作为教学案例,确实非常的简明易懂,但今天,我要好好地吐槽一下这种调用方法

4. 第一个问题

只要添加一个关键词,就得加好多行的代码,慢慢着,我添加的关键词越来越多,达到了几十个,而且大有愈演愈烈 的趋势,代码变得越来越臃肿,维护也变得越来越困难。这种无意义的重复代码,有代码洁癖的我看着是非常难受的。

@robot.filter('vip')
def get_vip_code(message):
    user_info = client.get_user_info(message.source)
    save_data(user_info["nickname"], "回复", message.content)
    return response.vip

@robot.filter('more')
def get_vip_code(message):
    user_info = client.get_user_info(message.source)
    save_data(user_info["nickname"], "回复", message.content)
    return response.more

@robot.filter('pdf')
def get_vip_code(message):
    user_info = client.get_user_info(message.source)
    save_data(user_info["nickname"], "回复", message.content)
    return response.pdf

5. 第二个问题

关键词的添加都需要通过 @robot.filter("关键词") 的方式来注册,而这种方式过于注重形式,对于一些人来说,可能是值得学习的优雅代码范例,但在无形之中,却又带来了两个致命的问题:

  1. 无法对关键词,实现批量注册
  2. 无法将关键词写在配置文件中,动态注册

6. 解决方案

Python 的装饰器语法糖 @ ,把很多人都带偏了,思维惯性地认为装饰器只能通过 @ 来调用。

想要解决装饰器带来的这一系列问题,我们就要回归装饰器的本质。

装饰器的本质就是一个函数,如果使用最原始的函数去注册关键词,那所有的问题都会迎刃而解。

下面这个是 robot 的 关键词注册装饰器的函数定义

def filter(self, *args):
    def wraps(f):
        self.add_filter(func=f, rules=list(args))
        return f

    return wraps

它的使用方法,我们上面也介绍过了。

@robot.filter("hello")
def hello(message):
    return 'Hello World!'

现在你可以想想,如果不使用 @ ,你会如何注册这个关键词呢?

我直接给出答案好了,你看装饰器调用的本质其实就是函数的连续调用。

# 等价于
robot.filter("hello")(hello)

一旦写成了这种形式,上面的两个问题就全部解决了,代码如下

# 消息处理函数
def lottery_draw(message):
    media_id = lottery_draw_info[message.content]["media_id"]
    article_url = lottery_draw_info[message.content]["article_url"]
    remark = lottery_draw_info[message.content]["remark"]
    client.send_image_message(message.source, media_id=media_id)
    client.send_text_message(message.source,
                             content='参与抽奖之前,请点击这个链接,阅读规则:<a href="{}">送红包规则,千万要看!</a>'.format(article_url))
    return remark

# 使用 for 循环从配置中读取关键词,然后进行批量注册
for code in lottery_draw_info.keys():
    robot.filter(code)(lottery_draw)

相应的配置信息我贴在下面,这样一来,我每次举办送书、送礼物的抽奖活动,都只要改一下我的配置文件,然后重启服务就可以了,完全不需要修改代码。

如何不使用 @,来调用 Python 装饰器插图(3)

7. 总结一下

为了避免有些同学,对这个装饰器理解不深,看了这篇文章后,就开始全盘否定装饰器,我想我还是得把饭喂到嘴边。

装饰器按照用途,我自己将它分为两种:

  • 一种是,注册性的装饰器。上面 Flask 的 route 和 WeRobot 的 filter 都属于这一类,这类装饰器,从服务启动最服务终止,都只会运行(注册)一次。
  • 一种是,功能性的装饰器。比如常见的,用来统计函数运行时长、或者函数运行超时退出的装饰器,这类装饰器,你每一次调用函数的时候都会触发,在整个服务的生命周期中,会调用很多次。

个人编写的装饰器基本都是第二种(功能性装饰器),而第一种注册性的装饰器通常是出现在框架中。

而本篇文章要表明的观点:使用函数的连续调用来代替装饰器的 @ 高级语法糖,是提高生产力的有力手段。 是针对 注册性的装饰器 而言的,功能性装饰器并不适用。

weixin

文章有帮助,请作者喝杯咖啡?

发表评论