装饰器
装饰器是 Python 中的一种特殊工具,他可以在不改变原函数的调用以及内部代码情况下,为其添加新功能的函数,简而言之就是一种修改其他函数的功能的函数。
先讨论几个python函数的概念
1. 函数中定义函数
首先我们讨论一下在函数中定义函数,示例如下
def f1():
print("now you are in the f1()")
def f2():
print("now you are in the f2()")
def f3():
print("now you are in the f3()")
f2()
f3()
print("now you are back to the f1()")
f1()
now you are in the f1()
now you are in the f2()
now you are in the f3()
now you are back to the f1()
以上代码中,在函数 f1 中定义了函数 f2 和 f3 ,在调用 f1 时,f2 和 f3 也会被调用。但在函数 f1 之外不能访问 f2 和 f3 函数,否则会报错。
2. 从函数中返回函数
函数也可以作为函数的返回值,示例如下:
def f1(num):
def f2():
print("now you are in the f2()")
def f3():
print("now you are in the f3()")
if num == 1:
return f2
else:
return f3
a = f1(1)
b = f1(2)
print(a, b) # 现在的a指向了f1函数中的f2函数
a() # 使用a(),f2函数就会执行
b() # 使用b(),f3函数就会执行
调用 f1 函数返回一个函数对象给 a,使用 a() 就能执行对应的函数;其他执行方式还可以是:f1(1)() 或 f1(2)() 。
3. 将函数作为参数传递
def hi():
return "你好"
def welcome(func):
print(func())
welcome(hi)
你好
创建第一个装饰器
def f1(func):
def f2():
print("now you are in f2()")
func()
print("now I have finished the func()")
return f2
def f3():
print("now you are in f3()")
f3()
f3 = f1(f3) # 现在f3就是被f2装饰的函数
f3()
now you are in f3()
now you are in f2()
now you are in f3()
now I have finished the func()
上面的代码虽然没有用到装饰器中的 @ 符号,但它描述的就是 python 中装饰器所做的事。接下来我们使用 @ 符号来构建装饰器的代码
def f1(func):
def f2():
print("now you are in f2()")
func()
print("now I have finished the func()")
return f2
@f1
def f3():
print("now you are in f3()")
f3()
now you are in f2()
now you are in f3()
now I have finished the func()
以上 @f1 就是对 f3 = f1(f3) 的简写
这里有个小问题需要解决
根据以上的写法会出现一个问题,当我们运行
print(f3.__name__)
时,会得到
f2
f3 的函数名怎么会是 f2 呢?原因是这里的函数被 f2 替代了。它重写了我们函数的名字包括注释文档(docstring)。
为了解决上面的问题,可以使用 python 中 functools.wraps 函数,具体方式如下:
from functools import wraps
def f1(func):
@wraps(func)
def f2():
print("now you are in f2()")
func()
print("now I have finished the func()")
return f2
@f1
def f3():
print("now you are in f3()")
print(f3.__name__)
得到
f3
@wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。
总结一下装饰器的语法
def 函数装饰器名称(func):
def 内嵌函数(*args, **kwargs):
需要添加的新功能
return func(*args, **kwargs)
return wrapper
@ 函数装饰器名称
def 原函数名称(参数):
函数体
原函数(参数)
示例:
from functools import wraps
def f1(func):
@wraps(func)
def f2(*args, **kwargs):
print("这是新的功能")
print(args[0])
return func(*args, **kwargs)
return f2
@f1
def f3(x):
print(x + x)
f3(1)
得到
这是新的功能
1
2
带参数的装饰器
其实上面用到的 @wraps 也是一个装饰器,但是它会像普通函数一样接受参数,那我们为什么不能构造一个带参数的装饰器?其实在使用 @f1 语法时,就是在执行一个以单个函数作为参数的包裹函数,要实现一个带参数的装饰器就需要编写一个能够返回包裹函数的函数,具体方法如下:
from functools import wraps
def f1(name):
def f2(func):
@wraps(func)
def f3(*args, **kwargs):
print("这是新加的功能")
print("你好", name)
return func(*args, **kwargs)
return f3
return f2
@f1("bob")
def f4(x):
print(x ** 2)
f4(3)
得到
这是新加的功能
你好 bob
9
这种方式很有用! 这么做在原本装饰器为原函数增加功能的基础上,又增加了一种修改原函数参数设置的效果,当然原函数结构是一点都不能动的。
使用类来实现装饰器
以上所讲的装饰器都是基于函数和闭包实现的,但这不是唯一的方式。事实上,Python对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)"的对象。以函数为例:
def f1():
pass
print(type(f1))
print(callable(f1)) # 使用 callable 可以检测某个对象是否是"可被调用的"
<class 'function'>
True
函数是一种”可被调用"的对象。除此之外,还可以让类变得“可被调用”,方法就是在类中实现__call__方法。如:
class MyClass1:
def __init__(self):
pass
class MyClass2:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
print("我是可被调用的")
a = MyClass1()
b = MyClass2()
print(callable(a))
print(callable(b))
b()
输出
False
True
我是可被调用的
基于这个特性,我们就可以使用类来实现装饰器
from functools import wraps
class MyClass:
def __init__(self):
pass
def __call__(self, func):
@wraps(func)
def wrapped_function(*args, **kwargs):
self.welcome()
return func(*args, **kwargs)
return wrapped_function
def welcome(self):
print("welocme to MyClass")
@MyClass()
def func1(x):
print(x ** 2)
func1(3)
welocme to MyClass
9
使用 wrapt 模块编写更扁平的装饰器
一般在写带参数的装饰器时,层层嵌套的函数难写又难读;另外,因为函数和类方法的不同,为函数写的装饰器无法直接用在类方法上,比如:
import random
def f1(a, b):
def f2(func):
def f3(*args, **kwargs):
print("这是新加的功能")
num = random.randint(a, b)
return func(num, *args, **kwargs)
return f3
return f2
@f1(1, 100)
def myfunc(num):
print(num)
class MyClass:
def __init__(self):
pass
@f1(1, 100)
def print_random_number(self, num):
print(num)
myfunc()
MyClass().print_random_number()
输出
这是新加的功能
36
这是新加的功能
<__main__.MyClass object at 0x7eff092140f0>
使用类实例方法会输出类实例,而不是期望的数字,出现这样结果的原因是类方法和函数两者在工作机制上有细微不同,如果要修复这个问题,必须跳过 *args 中的类实例 self 变量,才能将 num 作为第一个参数注入;这是就可以使用 wrapt 模块。
wrapt 模块是专门用来编写装饰器的工具库,它可以非常方便的改造装饰器,解决“函数嵌套深”和“无法通用”的问题。
示例如下:
import random
import wrapt
def gen_number(min_num, max_num):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
"""
wrapped:被装饰的函数或类方法
instance: 如果被装饰者是普通类方法,该值为类实例
如果被装饰者是 classmethod 类方法,该值为类
如果被装饰者是 类/函数/静态方法,该值为 None
args: 调用时的位置参数,没有 * 号
kwargs: 调用时的关键字参数,没有 ** 号
"""
num = random.randint(min_num, max_num)
return wrapped(num, *args, **kwargs)
return wrapper
class Foo:
@gen_number(1, 100)
def print_number(self, num):
print(num)
@gen_number(1, 100)
def print_number(num):
print(num)
Foo().print_number()
print_number()
42
38
使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,即使你不用它。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。
使用 wrapt 模块编写的装饰器有以下几个优点:
- 嵌套层少:将原来的两层嵌套减少为一层
- 更简单:处理位置和关键字参数时,可以忽略类实例等特殊情况
- 更灵活:使用 instance 进行条件判断后,更容易让装饰器通用