1.什么是装饰器
python中的装饰器是增强函数或类的功能的一个函数 / 类,是闭包(closure)的一种应用。
通过使用装饰器,我们可以在不修改原函数代码的前提下拓展函数的功能。
例如下面的代码中,func()
函数的上方的@demo
就是装饰器。
@demo
def func():
print('This is func')
2.为什么要使用装饰器
假设有这么一个函数
import time
def func1():
print("This is func1")
time.sleep(2)
现在,我们需要计算并输出这个函数执行所花费的时间。
使用传统方法改造函数,可能会是下面这个样子:
import time
def func1():
start_time = time.time()
print("This is func1")
time.sleep(2)
exec_time = time.time() - start_time
print('func1 :', exec_time)
func1()
-----------------------------
>>>This is func1
>>>func1 : 2.002577066421509
如果又有另外一个函数func2()
也同样需要计算时间,那就需要也对func2()
进行同样的改造。
import time
def func1():
start_time = time.time()
print("This is func1")
time.sleep(2)
exec_time = time.time() - start_time
print('func1 :', exec_time)
def func2():
start_time = time.time()
print("This is func2")
time.sleep(5)
exec_time = time.time() - start_time
print('func2 :', exec_time)
func1()
func2()
-----------------------------
>>>This is func1
>>>func1 : 2.002577066421509
>>>This is func2
>>>func2 : 5.000931262969971
像这样如果有n个函数需要改造,那就需要把同样的逻辑写n遍。这会带来几个问题:
- 工作量很大
- 如果计算时间的代码有改动,需要修改每个函数
- 降低了代码的可读性,不够简洁优雅
所以,我们需要更好的解决方法。
3.如何使用装饰器
回到上面的例子,这次我们使用闭包来实现功能。
import time
def time_calc(func):
def wrapper():
start_time = time.time()
f = func()
exec_time = time.time() - start_time
print('{} :{}'.format(func.__name__, exec_time))
return f
return wrapper
def func1():
print("This is func1")
time.sleep(2)
def func2():
print("This is func2")
time.sleep(5)
decorated1 = time_calc(func1)
decorated1()
decorated2 = time_calc(func2)
decorated2()
-----------------------------
>>>This is func1
>>>func1 :2.0153732299804688
>>>This is func2
>>>func2 :5.0016069412231445
可以看到在time_calc()
函数内,又定义了一个wrapper()
函数,并且wrapper()
函数又引用了外部函数time_calc()
的变量func
,形成了一个闭包。
使用时,我们首先将真正要执行的函数(例子中的func1()
或者func2()
)作为参数传递给time_calc()
函数。time_calc()
函数在其前后增加额外的逻辑后再将包装好的函数返回给我们。我们通过执行包装好的函数实现所有功能。
相比直接修改函数,显然使用闭包的方案代码复用率更高,也更易于维护。
但是这也带来了新的问题:执行函数前必须先对函数进行包装,然后执行包装后的函数。
decorated1 = time_calc(func1)
decorated1()
为了解决这一问题,Python 2.4通过在函数定义前添加一个装饰器名和@符号,来实现对函数的自动包装。
下面是装饰器方案的代码:
import time
def time_calc(func):
def wrapper():
start_time = time.time()
f = func()
exec_time = time.time() - start_time
print('{} :{}'.format(func.__name__, exec_time))
return f
return wrapper
@time_calc
def func1():
print("This is func1")
time.sleep(2)
@time_calc
def func2():
print("This is func2")
time.sleep(5)
func1()
func2()
-----------------------------
>>>This is func1
>>>func1 :2.011098861694336
>>>This is func2
>>>func2 :5.007617473602295
可以看到,在函数定义的上方添加@time_calc
后,无需手动包装,直接执行原函数就可以达到一样的效果。
这是因为@time_calc
等同于下面的代码:
func1 = time_calc(func1)
装饰器还可以嵌套
@deco1
@deco2
@deco3
def func()
pass
装饰器嵌套时执行的顺序为由下至上,上面的代码等同于:
func = deco1(deco2(deco3(func)))
4.带参数的函数与带参数的装饰器
很多时候(也许是绝大多数时候)我们执行的函数是需要传入参数的。例如:
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
这时候,我们需要修改装饰器使其可以接收函数的参数。
import time
def time_calc(func):
def wrapper(x, y):
start_time = time.time()
f = func(x, y)
exec_time = time.time() - start_time
print('{} :{}'.format(func.__name__, exec_time))
return f
return wrapper
@time_calc
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
func_add(1, 2)
-----------------------------
>>>x + y = 3
>>>func_add :1.0004308223724365
由于一个装饰器往往要对应许多个不同的函数,而这些函数又很有可能拥有不同的参数数量。为了提高泛用性,一般使用*args
和**kwargs
来取代具体的参数和键值对。
import time
def time_calc(func):
def wrapper(*args, **kwargs):
start_time = time.time()
f = func(*args, **kwargs)
exec_time = time.time() - start_time
print('{} :{}'.format(func.__name__, exec_time))
return f
return wrapper
@time_calc
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
@time_calc
def func_add_three(x, y, z):
print('x + y + z = ', x+y+z)
time.sleep(2)
func_add(1, 2)
func_add_three(4, 5, 6)
-----------------------------
>>>x + y = 3
>>>func_add :1.0120115280151367
>>>x + y + z = 15
>>>func_add_three :2.0111453533172607
另外,我们也可以对装饰器本身传递参数,只需在原有的基础上再嵌套一层即可。
例如下面代码中的装饰器能够接收一个level
参数并将参数的值输出到打印结果中。
import time
def time_calc(level):
def outwrapper(func):
def wrapper(*args, **kwargs):
start_time = time.time()
f = func(*args, **kwargs)
exec_time = time.time() - start_time
print('{} {} :{}'.format(level, func.__name__, exec_time))
return f
return wrapper
return outwrapper
@time_calc(level="INFO")
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
@time_calc(level="DEBUG")
def func_add_three(x, y, z):
print('x + y + z = ', x+y+z)
time.sleep(2)
func_add(1, 2)
func_add_three(4, 5, 6)
-----------------------------
>>>x + y = 3
>>>INFO func_add :1.015167236328125
>>>x + y + z = 15
>>>DEBUG func_add_three :2.0142157077789307
5.类装饰器
除了函数,还可以使用类作为装饰器。
import time
class TimeCalculator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start_time = time.time()
f = self.func(*args, **kwargs)
exec_time = time.time() - start_time
print('{} :{}'.format(self.func.__name__, exec_time))
return f
@TimeCalculator
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
@TimeCalculator
def func_add_three(x, y, z):
print('x + y + z = ', x+y+z)
time.sleep(2)
func_add(1, 2)
func_add_three(4, 5, 6)
-----------------------------
>>>x + y = 3
>>>func_add :1.0122718811035156
>>>x + y + z = 15
>>>func_add_three :2.006585121154785
用法与函数装饰器并没有太大区别,实质是使用了类方法中的__call__()
方法来实现类的直接调用。
当然,类装饰器也是可以带参数的:
import time
class TimeCalculator:
def __init__(self, level):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
start_time = time.time()
f = func(*args, **kwargs)
exec_time = time.time() - start_time
print('{} {} :{}'.format(self.level, func.__name__, exec_time))
return f
return wrapper
@TimeCalculator(level='INFO')
def func_add(x, y):
print('x + y = ', x+y)
time.sleep(1)
@TimeCalculator(level='DEBUG')
def func_add_three(x, y, z):
print('x + y + z = ', x+y+z)
time.sleep(2)
func_add(1, 2)
func_add_three(4, 5, 6)
此时__init__()
方法负责接收传递给类装饰器的参数,__call__()
方法负责接收要执行的函数。
在__call__()
方法里面增加了一层嵌套用来接收要执行的函数的参数。
相比函数装饰器,类装饰器具有一定的优势。
比如可以在类中定义多个函数以对应不同的用途,还可以利用类的成员变量来储存信息等。
另外由于是类,当然也可以被继承。
所以如果需要实现的功能比较复杂,也许使用类装饰器是一个更好的选择。
下面是一个缓存装饰器的例子。
- 使用函数名和参数生成key,保存对应的函数执行结果到成员变量
data
里。 - 如果拥有相同参数的函数已经执行过了,则直接返回成员变量
data
里的结果,并打印“cache result”。 - 如果没有执行过,则执行函数返回结果,并打印“calculation result”。
class Cacher:
def __init__(self, func):
self.func = func
self.data = {}
def __call__(self, *args, **kwargs):
key = f'{self.func.__name__}-{str(args)}-{str(kwargs)})'
if key in self.data:
result = self.data.get(key)
print('cache result')
else:
result = self.func(*args, **kwargs)
self.data[key] = result
print('calculation result')
return result
@Cacher
def func_add(x, y):
return x + y
print(func_add(1, 2))
print(func_add(3, 4))
print(func_add(1, 2))
-----------------------------
>>>calculation result
>>>3
>>>calculation result
>>>7
>>>cache result
>>>3
此外,类的实例对象也可以作为装饰器使用,在这里就不举例说明了。
6.python的几种内置装饰器
python已经为我们内置了几种装饰器,可以直接使用。
@property
property装饰器可以把一个实例方法变成其同名属性,以支持实例访问,它返回的是一个property属性。
例如以下代码中给方法x()
添加@property
装饰器后就可以像访问数据属性一样去访问它。
class C:
def __init__(self, x=None):
self._x = x
@property
def x(self):
return self._x
c = C(100)
print(c.x)
-----------------------------
>>>100
一个property对象还具有setter、deleter和getter装饰器。
- setter用于设置属性值
- deleter用于删除属性值
- getter用于获取属性值(但是因为可以直接通过property获取属性信息所以一般不太有人用它)
下面的代码在初始化后分别执行了对属性_x
的赋值和删除。执行最后一行print(c.x)
时,由于属性_x
已经被删除,所以程序输出了出错信息。
class C:
def __init__(self, x=None):
self._x = x
@property
def x(self):
return self._x
@x.setter
def x(self, value):
print('set _x to', value)
self._x = value
@x.deleter
def x(self):
print('delete _x')
del self._x
c = C()
print(c.x)
c.x = 100
print(c.x)
del c.x
print(c.x)
-----------------------------
>>>None
>>>set _x to 100
>>>100
>>>delete _x
>>>Traceback (most recent call last):
...
AttributeError: 'C' object has no attribute '_x'
@classmethod
被classmethod装饰器后方法可以直接通过类名来直接调用,而无需建立实例。
与实例方法需要通过self
参数隐式的传递当前类对象的实例相似,@classmethod
修饰的方法也需要通过第一参数cls
传递当前类对象。
由于是类方法,只能访问类属性,不能访问实例方法和实例属性。
class C:
classParam = 'classparam'
def __init__(self, x=None):
self._x = x
def printInstanceParam (self):
print(self._x)
@classmethod
def printClassParam(cls):
print(cls.classParam)
C.printClassParam()
C.printInstanceParam()
-----------------------------
>>>classparam
>>>Traceback (most recent call last):
...
TypeError: printInstanceParam() missing 1 required positional argument: 'self'
@staticmethod
staticmethod装饰器可以将类方法改变为静态方法。
静态方法不需要传递隐性的第一参数,可以直接通过类进行调用,也可以通过实例进行调用。
静态方法的本质类型就是一个函数,只是寄存在一个类名下。
class C(object):
@staticmethod
def func():
print('run func');
C.func();
cobj = C()
cobj.func()
-----------------------------
>>>run func
>>>run func