Python装饰器
现在,我们来定义一个函数,fight。这个函数需要3个参数,color,time,o,分别是颜色、时间、某个对象。
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
玩过moba游戏的都知道这是游戏开头的语音播报,我们就以这个举例。
现在我有一个需求,让你计算一下这个函数的运行时间,但是不能修改函数的内部代码。你会怎么做?
方案1
单凭你的直觉,不要想太多,你肯定立马就能想到,这也太简单了,只需要在函数的前后计算一下时间,然后算一下间隔就可以了。
# encoding:utf-8 import time def fight(color,t,o): print(f'我们出生在{color}方阵营') print(f'敌军还有{t}秒到达战场') print(f'{o}出击') start = time.time() time.sleep(2) fight('红色','27','小许') end = time.time() print(end-start)
(由于案例的函数太简单,运行时间几乎是瞬间完成,所以用time模块睡了2秒)
这样就得到了函数的运行时间,很简单吧。
但是,实际工作中,需求肯定不是这么简单的,我们这个方法对单独一个函数这样操作没问题,实际工作中,我们可能要给大量的函数添加时间计算的功能,你总不能,每调用一次函数,就手动敲这些代码吧,作为程序员,不够优雅与方便,这种方案不可取。
方案2
既然有重复代码了,那我就用函数来解决重复的部分。经过一阵思考后:# encoding:utf-8
import time
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
def wrapper(color,t,o):
start = time.time()
time.sleep(2)
fight(color,t,o)
end = time.time()
print(end-start)
wrapper("红色","27","小许")
评价:这下我们把fight函数的参数写活了,让他用外部的wrapper函数的参数,这样就方便多了
但是问题,依旧存在
假如fight函数后续还需要其他参数呢?我们当初写这个函数的时候,只是初步传入了一些参数,后续的改动中,很可能需要加入新的参数。这是很常见的。
方案3
这次你发狠了,行行行,要参数是吧,我就给你参数。
你想到了之前学习的可变位置参数、可变关键字参数,这里,你终于用上了
# encoding:utf-8
import time
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
fight(*args,*kwargs)
end = time.time()
print(end-start)
wrapper("红色","27","小许")
wrapper调用的时候,会先把传入的3个实参:红色、27、小许,打包为元祖(“红色”,“27”,“小许”)。
内层函数fight调用的时候,会把元祖给打散,依次拿到红色、27、小许这3个参数,这样就可以实现了随便传参的目的,
评价:这一次,我们的方案终于很强了,可以随便传参,而且还不修改被装饰对象fight的代码。太强了!
但是有个问题,我们写的计时用的装饰器,只能为fight函数服务,其他函数享用不到,怎么让其他函数享用到呢。
就需要把fight函数给写活了。所以,我们还要继续优化我们的方案!
方案4:
怎么把函数wrapper写活呢,让它能为其他任何函数服务呢?
经过思考,我们发现:
想把它写活,就需要把函数当做变量用。
# encoding:utf-8 import time def fight(color,t,o): print(f'我们出生在{color}方阵营') print(f'敌军还有{t}秒到达战场') print(f'{o}出击') func = fight def wrapper(*args,**kwargs): start = time.time() time.sleep(2) func(*args,*kwargs) end = time.time() print(end-start) wrapper("红色","27","小许")
我们在wrapper外部定义一个变量 func,用它来接受fight的值,注意fight不要加括号,加了括号就等于调用函数了,我们这里只要fight的函数内存地址。
评价:这一次,我们使用一个变量func来接收被装饰函数的内存地址,大大增加了灵活性。问题依旧存在,
每次装饰某个函数,都需要先手动敲一下func=被装饰函数,然后再调用装饰函数,不够优雅,甚至是麻烦。
TIPS:
函数加了括号,等于调用了;单独的函数名,得到的是函数的内存地址。
方案5:
我们继续思考,之前为了给fight函数增加功能,我们在外部包了一层函数wrapper。这次我们还想给wrapper增加功能,那就如法炮制吧。
# encoding:utf-8
import time
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
def outter():
func = fight
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
func(*args,*kwargs)
end = time.time()
print(end-start)
return wrapper # 把wrapper的内存地址返回去
res = outter() #调用outter函数,返回wrapper的内存地址
res("红色","27","小许")
print(res) # res的值是wrapper的内存地址
这一次,我们在outter函数内部放了一个变量func来接收被被装饰函数fight的内存地址。
我们用res来接收outter()的返回值,outter()的返回值是wrapper的内存地址。
所以,我们使用 res("红色","27","小许") 调用函数,其实就是被装饰函数func在执行
不过,还是有些小问题,因为我们把 func = fight放在outter函数内部了,这相当于把函数写死了,所以为了增加灵活性,我们需要把func放在outter的参数里。
# encoding:utf-8
import time
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
func(*args,*kwargs)
end = time.time()
print(end-start)
return wrapper # 把wrapper的内存地址返回去
res = outter(fight) #调用outter函数,返回wrapper的内存地址
res("红色","27","小许")
print(res) # res的值是wrapper的内存地址
以后使用outter函数的时候,只需要给它传入一个函数的函数名(也就是函数的内存地址),不要带括号。
方案6
现在,还有一个问题,被我们忽略了 ,如果被装饰函数有返回值呢?
返回值是很重要的,丢失了返回值,是很严重的后果。
我们再来优化一下,很简单,用一个变量来接收被装饰函数的返回值,再把这个变量return出来就可以了。
# encoding:utf-8
import time
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
result = func(*args,*kwargs)
end = time.time()
print(end-start)
return result
return wrapper # 把wrapper的内存地址返回去
res = outter(fight) #调用outter函数,返回wrapper的内存地址
res("红色","27","小许")
print(res) # res的值是wrapper的内存地址
现在,还有一个小问题,我们每次使用outter的时候,都需要先进行赋值:fight = outter(fight),把被装饰函数fight的函数名放在参数里,总是写这一行代码,也不够优雅。
因此,Python为我们提供了一个简便的操作,就是语法糖。
我们只需要在被装饰函数的头部,写上 @outter 就可以了。
@outter
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
return "你真是个小天才"
这就是装饰器!在不修改被装饰函数的代码的情况下,给被装饰函数增加扩展功能,
@decorator # 装饰器
def self_defined_function(): # 被装饰函数
print("Self-defined function is called.")
1. 什么时候需要用装饰器?
试想你有一系列函数,例如如下所示的两个函数(分别计算和与积):
def my_sum_function(*args):
print("The sum is", sum(args))
def my_product_function(*args):
res = 1
for x in args:
res *= x
print("The product is", res)
my_sum_function(1, 2, 3, 4)
my_product_function(1, 2, 3, 4, 5)
现在,你需要为这一系列函数添加一个一样的功能,就是统计输入参数的个数,同时检查里面是否有0,那么低阶的解决方法就是为每一个函数单独添加部分代码,即
def my_sum_function(*args):
print("No. of input args is", len(args)) # 重复部分
contain_zero = any([x == 0 for x in args]) # 重复部分
print("Input arguments contain 0:", contain_zero) # 重复部分
print("The sum is", sum(args))
def my_product_function(*args):
print("No. of input args is", len(args)) # 重复部分
contain_zero = any([x == 0 for x in args]) # 重复部分
print("Input arguments contain 0:", contain_zero) # 重复部分
res = 1
for x in args:
res *= x
print("The product is", res)
my_sum_function(1, 2, 3, 4)
my_product_function(0, 1, 2, 3, 4, 5)
这样写的话,代码中存在大量重复的部分,有没有好一点的办法呢?也许你已经想到了,就是把添加的这部分功能封装成一个单独的函数,然后让每一个函数单独调用这个函数(嵌套调用),这样就能减少写代码过程中的“Ctrl+C和Ctrl+V”,即
def my_sum_function(*args): additional_function(*args) # 嵌套调用 print("The sum is", sum(args)) def my_product_function(*args): additional_function(*args) # 嵌套调用 res = 1 for x in args: res *= x print("The product is", res) def additional_function(*args): print("No. of input args is", len(args)) contain_zero = any([x == 0 for x in args]) print("Input arguments contain 0:", contain_zero) my_sum_function(1, 2, 3, 4) my_product_function(0, 1, 2, 3, 4, 5)
有没有更好的办法呢 ? 那就是 装饰器闪亮登场。
def a_decorator(f): #装饰器代码 def additional_function(*args): print("No. of input args is", len(args)) contain_zero = any([x == 0 for x in args]) print("Input arguments contain 0:", contain_zero) f(*args) return additional_function @a_decorator def my_sum_function(*args): #被装饰函数代码 print("The sum is", sum(args)) @a_decorator def my_product_function(*args): #被装饰函数代码 res = 1 for x in args: res *= x print("The product is", res) my_sum_function(1, 2, 3, 4) my_product_function(0, 1, 2, 3, 4, 5)
2. 装饰器的执行机制
装饰器的使用场景:就是为函数定制化额外功能的时候,可是添加装饰器。使用装饰器可以使代码更加简洁。那么装饰器是如何工作的呢?我们以如下例子说明:
def a_decorator(f): # 函数作为参数被传入
print(f"Function {f.__name__} is passed as the augument!")
def additional_function(*args): # 函数嵌套
print("No. of input args is", len(args))
contain_zero = any([x == 0 for x in args])
print("Input arguments contain 0:", contain_zero)
f(*args) # 最终执行目标函数my_sum_function的地方
return additional_function # 函数作为返回对象
@a_decorator
def my_sum_function(*args):
print("The sum is", sum(args))
my_sum_function(1, 2, 3, 4)运行结果如下$ python demo.py
Function my_sum_function is passed as the augument!
No. of input args is 4
Input arguments contain 0: False
The sum is 10
- 一、在Python中,一切皆对象,包括函数。一个函数可以作为另一个函数的参数,一个函数也可以作为另一个函数的返回对象;
- 二、如果在一个函数体中定义了另一个函数,称为函数嵌套,前者称为enclosing function,后者称为enclosed function或者nested function
也就是说,上述例子如果不用@符号,和下面是完全等价的
def a_decorator(f):
print(f"Function {f.__name__} is passed as the augument!")
def additional_function(*args):
print("No. of input args is", len(args))
contain_zero = any([x == 0 for x in args])
print("Input arguments contain 0:", contain_zero)
f(*args)
return additional_function
def my_sum_function(*args):
print("The sum is", sum(args))
a_decorator(my_sum_function)(1, 2, 3, 4) # 注意理解这一行
3. 闭包
在上述例子中,对于a_decorator函数的设计使用了闭包(closure)的概念[1],即
- 一个函数中嵌套了另一个函数,如上述a_decorator函数中嵌套了additional_function函数
- enclosed function中直接使用了enclosing funcion中的参数,如上述additional_function函数中使用了a_decorator函数的参数f
- 最终返回enclosing funcion,如上述例子中最终返回了additional_function
满足以上三个条件的称为Python闭包。
4. 函数名被重写了怎么办?用functools.wraps!
在上述例子中,如我们print(my_sum_function.__name__),会发现
def a_decorator(f): # 函数作为参数被传入
print(f"Function {f.__name__} is passed as the augument!")
def additional_function(*args): # 函数嵌套
print("No. of input args is", len(args))
contain_zero = any([x == 0 for x in args])
print("Input arguments contain 0:", contain_zero)
f(*args) # 最终执行目标函数my_sum_function的地方
return additional_function # 函数作为返回对象
@a_decorator
def my_sum_function(*args):
print("The sum is", sum(args))
print(my_sum_function.__name__)
# output: additional_function
my_sum_function函数的名字被重写了,怎么解决这个问题呢,很简单,调用functools.wraps即可[2],即
from functools import wraps def a_decorator(f): # 函数作为参数被传入 print(f"Function {f.__name__} is passed as the augument!") @wraps(f) # 注意这一行 def additional_function(*args): # 函数嵌套 print("No. of input args is", len(args)) contain_zero = any([x == 0 for x in args]) print("Input arguments contain 0:", contain_zero) f(*args) # 最终执行目标函数my_sum_function的地方 return additional_function # 函数作为返回对象 @a_decorator def my_sum_function(*args): print("The sum is", sum(args)) print(my_sum_function.__name__) # output: my_sum_function
5. 如何给装饰器传入参数?带参装饰器!
如果我想给装饰器再添加一个功能,就是把目标函数(my_sum_function)的计算结果存入本地文件,本地文件的文件名可以作为参数随时进行修改。这时候就需要用到带参装饰器了,那么带参装饰器如何实现呢?其实很简单,就是再嵌套一层,即
from functools import wraps
filename = "output.txt"
def a_decorator(filename):
print(filename)
def inter_function(f): # 第一层嵌套
@wraps(f)
def additional_function(*args): # 第二层嵌套
print("No. of input args is", len(args))
contain_zero = any([x == 0 for x in args])
print("Input arguments contain 0:", contain_zero)
result = f(*args)
with open(filename, "w") as txt:
txt.write(result)
txt.close()
return additional_function
return inter_function
@a_decorator(filename) # 带参装饰器
def my_sum_function(*args):
print("The sum is", sum(args))
return str(sum(args))
my_sum_function(1, 2, 3)
# 执行完毕会在本地创建output.txt文件同时写入结果
在调用functools.wraps时,@wraps(f)其实就是一个带参装饰器。
6. 类装饰器
除了函数可以作为装饰器,类也可以作为装饰器,这时候就需要类可以作为函数进行调用。如何实现把类作为函数使用呢?答案是__call__魔法函数,如
from functools import wraps class A_decorater: def __init__(self) -> None: pass def __call__(self, f): @wraps(f) def additional_function(*args): print("No. of input args is", len(args)) contain_zero = any([x == 0 for x in args]) print("Input arguments contain 0:", contain_zero) f(*args) return additional_function @A_decorater() # 千万注意这里括号不能少,因为A_decorater是类,A_decorater()是类的实例 def my_sum_function(*args): print("The sum is", sum(args)) return str(sum(args)) my_sum_function(1, 2, 3)
如果要带参数的话,同样地需要再嵌套一层,即
from functools import wraps filename = "output.txt" class A_decorater: def __init__(self) -> None: pass def __call__(self, filename): print(filename) def inter_function(f): @wraps(f) def additional_function(*args): print("No. of input args is", len(args)) contain_zero = any([x == 0 for x in args]) print("Input arguments contain 0:", contain_zero) result = f(*args) with open(filename, "w") as txt: txt.write(result) txt.close() return additional_function return inter_function a_decorater = A_decorater() # 不可避免,还是要先生成一个实例 @a_decorater(filename) def my_sum_function(*args): print("The sum is", sum(args)) return str(sum(args)) my_sum_function(1, 2, 3)
原文出处:
https://www.zhihu.com/question/271201015/answer/2387427580
https://www.zhihu.com/question/271201015/answer/3178585157