作者:无尘粉笔
链接:https://www.zhihu.com/question/271201015/answer/2387427580
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
装饰器(decorator)是Python一个进阶的用法,通常以如下形式出现,但是它到底怎么用呢?且看本文细细分解。
@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)
那有没有更pythonic的写法呢?这里就可以用到装饰器了,实现方法如下
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
根据运行结果,我们可以反推出装饰器的工作机制:
- my_sum_function作为参数传入a_decorator函数,并开始执行,因此首先打印出“Function my_sum_function is passed as the augument!”
- 随后a_decorator函数执行过程中返回了additional_function函数对象,然后开始执行additional_function(1, 2, 3, 4),于是打印出“No. of input args is 4”和“Input arguments contain 0: False”
- 在additional_function函数嵌套调用了my_sum_function函数,因此最后打印“The sum is 10”
也就是说,上述例子如果不用@符号,和下面是完全等价的:
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)
参考
- ^Python装饰器、闭包 9. Python 装饰器、闭包 — openstack 1.0 documentation
- ^Python 函数装饰器 Python 函数装饰器 | 菜鸟教程
作者:无尘粉笔
链接:https://www.zhihu.com/question/271201015/answer/2387427580
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。