59、Python之函数高级:带参数的装饰器实现更加灵活的增强效果

引言

前面我们通过简单的闭包实现了装饰器的增强效果,基于不定长参数的传递,实现了装饰器的更加通用的写法。本文我们接着介绍装饰器的使用,通过介绍带参数的装饰器,从而实现更加灵活的装饰器,从而进一步提升代码的复用性。在学习这些技巧或者特性的时候,只要能够理解到:什么场景下能用到,对代码的可扩展性和复用性有没有帮助,就能够更加容易理解到相关的本质,而无需强行记忆。

本文的主要内容有:

1、再看装饰器的本质

2、有欠考虑的案例

3、带参数的装饰器

再看装饰器的本质

其实,简单回忆一下前面用到的装饰器,我们应该有以下关于装饰器的认知:

1、装饰器是一个高阶函数,其作用对象是以参数形式传入的需要装饰/增强的原始函数。

2、装饰器返回的是一个一阶函数,因为在用户端调用要做到无感知的,所以返回的一阶函数是可以近似等价于对原始函数的调用的。

3、@装饰器名这个语法糖,本质上就是调用装饰器这个高阶函数,并以装饰器返回的函数对象,替换原始函数名与原始函数对象的绑定关系。也就是说,“名称绑定”关系,由 原始函数名 -> 原始函数对象, 更改为:原始函数名 -> 装饰器返回的函数对象。

所以,装饰器这个高阶函数,本质上是一个二阶函数。可以粗略的理解为,对该函数可以进行“两次调用”(其实,一次是对装饰器调用,第二次是对装饰器返回的函数对象的调用)。

以此类推的话,相信聪明的读者,立马就能理解三阶函数,甚至是更高阶函数的使用……

之所以要这样回顾总结,是因为今天要介绍的内容会涉及到三阶函数。

一个不太敞亮的案例

基于笔者一贯的理念,在介绍一个新的编程语言的特性或者使用技巧之前,还是首先要找到我们在工作中能够使用的场景,尽量避免学到无用的知识,白白浪费了时间(当然,在笔者看来,没有无用的知识,只有读者有没有能力把学到的知识变得有用……)。

有这样一个案例,我们希望开发的系统能够真正赚到钱,但是,用户都很聪明的。所以,业界通常的习惯,都是先以免费的版本,进行用户的使用习惯培育,然后再考虑付费用户的转换。

接下来我们以一个不太敞亮的案例,模拟一个软件版本的实现:

import time


def feature():
    print("这是正常执行功能的模块")


def free(func):
    def wrap(*args, **kwargs):
        print("免费用户正在排队...")
        time.sleep(5)
        return func(*args, **kwargs)

    return wrap


def vip(func):
    def wrap(*args, **kwargs):
        print("vip用户正在使用高速通道...")
        time.sleep(2)
        return func(*args, **kwargs)

    return wrap


def svip(func):
    def wrap(*args, **kwargs):
        print("svip用户正在使用专属通道...")
        time.sleep(0)
        return func(*args, **kwargs)

    return wrap


if __name__ == '__main__':
    feature_free = free(feature)
    feature_vip = vip(feature)
    feature_svip = svip(feature)

    feature_free()
    feature_vip()
    feature_svip()

执行结果:

84c1252f270cd1b596e608a967eb1b4d.jpeg

feature()函数是一个系统的功能,实际系统会有很多个功能。将用户区分为三种:免费用户、普通vip用户、超级vip用户。

考虑到功能可能很多,所以定义了3个装饰器函数分别为:free、vip、svip。其实就是等待的时长不同,代码逻辑可能都一样。

三个装饰器除了time.sleep()的时长不同,其他几乎都一样,存在明显重复的代码,我们能否将这些重复的代码消除呢?答案是肯定的,这就要用到我们本文的主角:带参数的装饰器。

带参数的装饰器

上面的不太敞亮的这个案例,虽然最终也能实现效果了,但是,我们也看到代码的重复。只是因为想着能够灵活处理不同付费用户体验,就重复定义了几个同样的装饰器,严重违反了DRY(Don't Repeat Yourself)的原则。

这时,聪明的读者一定会想到,如果我们能够把需要灵活变动的部分,作为参数封装起来,就像函数传参一样,使用同一个装饰器,传入不同的参数,实现不同的效果。

没错,所谓“带参数的装饰器”就是这样的实现思路。

但是,首先,装饰器是一个二阶函数,返回的内嵌函数在参数传递与返回值上要保持一致。

那么我们可以尝试一下在外部函数上添加一个参数来看看:

import time


def user_level(func, level):
    latency = {'free': 5, 'vip': 2, 'svip': 0}
    message = {
        'free': '免费用户正在排队...',
        'vip': 'vip用户正在使用高速通道...',
        'svip': 'svip用户正在使用专属通道...',
    }

    def wrap(*args, **kwargs):
        print(message[level])
        time.sleep(latency[level])
        return func(*args, **kwargs)

    return wrap


def feature():
    print("这是正常执行功能的模块")


if __name__ == '__main__':
    feature_free = user_level(feature, 'free')
    feature_vip = user_level(feature, 'vip')
    feature_svip = user_level(feature, 'svip')

    feature_free()
    feature_vip()
    feature_svip()

执行结果:

a838d15239fa4bef813ced1db1d9f555.jpeg

看来是可以的,似乎也能满足我们的需求了。但是呢,有一个问题是Python的@装饰器名的语法糖用不了了:

@user_level
def feature():
    print("这是正常执行功能的模块")

执行报错:

93c0dc3dda6ef8225a7c67f137afc581.jpeg

这是因为,@装饰器名的用法等价于 feature = user_level(feature)的这种写法,而这种情况下,要求装饰器函数只能有一个参数,也就是被装饰的函数对象。

而真正的带参数的装饰器,是支持@装饰器名(参数)的写法的。

我们直接来看代码:

import time


def user_level(level):
    def user_level_inner(func):
        latency = {'free': 5, 'vip': 2, 'svip': 0}
        message = {
            'free': '免费用户正在排队...',
            'vip': 'vip用户正在使用高速通道...',
            'svip': 'svip用户正在使用专属通道...',
        }

        def wrap(*args, **kwargs):
            print(message[level])
            time.sleep(latency[level])
            return func(*args, **kwargs)

        return wrap

    return user_level_inner


@user_level('vip')
def feature():
    print("这是正常执行功能的模块")


if __name__ == '__main__':
    feature()

注意,这里带参数的装饰器实质上是一个三阶函数,@装饰器(参数)的写法,首先会使用参数调用第一阶函数,返回一个之前的无参的装饰器函数;然后用这个无参的装饰器函数对原始函数进行真正的装饰。

@user_level('vip') 的写法,实质上执行的代码是:

feature = user_level('vip')(feature)。

需要注意的是:

1、如果需要使用@装饰器(参数)的形式来使用有参数的装饰器,那么就需要使用三阶函数,只需要理解@后面的内容返回的一定是一个函数对象,这个函数对象是用来接收要装饰的函数的。

2、如果不需要使用@装饰器的语法糖,则只需要定义一个带参数的二阶函数就行,其实多写的代码也不算多,很灵活,易于理解。但是,相对来说,不如@语法糖方法。

所以,如果真正理解了语法糖、装饰器、高阶函数、闭包的内涵,真正用起来,可以更加自由的选择。

总结

本文首先简单回顾了装饰器的相关知识点,更加理解了装饰器本质上是一个高阶函数的概念。然后,通过一个不太恰当的案例,引出带参数的装饰器的需求。最后,通过带参数的装饰器以及一种变通的写法,实现案例的需求。

感谢您的拨冗阅读,希望对您有所帮助。

54860b10ff379f6eeee4e193a2d0a991.jpeg

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值