引言
前面我们通过简单的闭包实现了装饰器的增强效果,基于不定长参数的传递,实现了装饰器的更加通用的写法。本文我们接着介绍装饰器的使用,通过介绍带参数的装饰器,从而实现更加灵活的装饰器,从而进一步提升代码的复用性。在学习这些技巧或者特性的时候,只要能够理解到:什么场景下能用到,对代码的可扩展性和复用性有没有帮助,就能够更加容易理解到相关的本质,而无需强行记忆。
本文的主要内容有:
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()
执行结果:
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()
执行结果:
看来是可以的,似乎也能满足我们的需求了。但是呢,有一个问题是Python的@装饰器名的语法糖用不了了:
@user_level
def feature():
print("这是正常执行功能的模块")
执行报错:
这是因为,@装饰器名的用法等价于 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、如果不需要使用@装饰器的语法糖,则只需要定义一个带参数的二阶函数就行,其实多写的代码也不算多,很灵活,易于理解。但是,相对来说,不如@语法糖方法。
所以,如果真正理解了语法糖、装饰器、高阶函数、闭包的内涵,真正用起来,可以更加自由的选择。
总结
本文首先简单回顾了装饰器的相关知识点,更加理解了装饰器本质上是一个高阶函数的概念。然后,通过一个不太恰当的案例,引出带参数的装饰器的需求。最后,通过带参数的装饰器以及一种变通的写法,实现案例的需求。
感谢您的拨冗阅读,希望对您有所帮助。