装饰器概述

Python的装饰器本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。Python的装饰器广泛应用于缓存权限校验(如django中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。

装饰器实际上就是为了给某程序增添功能,但该程序已经上线或已经被使用,那么就不能大批量的修改源代码,这样是不科学的也是不现实的,因为就产生了装饰器,使得其满足:

  • 不能修改被装饰的函数的源代码;
  • 不能修改被装饰的函数的调用方式;
  • 满足1、2的情况下给程序增添功能。

那么根据需求,同时满足了这三点原则,这才是我们的目的。

装饰器的开放封闭原则:

  • 开放:指的是对拓展功能是开放的;
  • 封闭:指的是对修改源代码是封闭的。

装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰对象添加新功能

首先,来看看装饰器的原则组成:
< 函数 + 实参高阶函数 + 返回值高阶函数 + 嵌套函数 + 语法糖 = 装饰器 >
这个式子就是贯穿装饰器的灵魂所在!

Python装饰器的工作原理主要依赖于嵌套函数闭包。所以在此之前,先来学习一些前置知识。

前置语法知识

函数“变量” / “变量”函数

函数和变量是一样的,都是“一个名字对应内存地址中的一些内容”。为方便理解,假设有如下代码:

  1. def test1():
  2. print("Do something")
  3. test2 = lambda x:x * 2

那么根据这样的原则,我们就可以理解两个事情:

  1. test1表示的是函数的内存地址;
  2. test1()就是调用对在test1这个地址的内容,即函数。

高阶函数

对于高阶函数的形式可以有两种:

  1. 把一个函数名当作实参传给另外一个函数(“实参高阶函数”);
  2. 返回值中包含函数名(“返回值高阶函数”)。

这里所说的函数名,实际上就是函数的地址,也可以认为是函数的一个标签而已,并不是调用,是个名词。如果可以把函数名当做实参,那么也就是说可以把函数传递到另一个函数,然后在另一个函数里面做一些操作,根据这些分析来看,这岂不是满足了装饰器三原则中的第一条,即不修改源代码而增加功能。

来看个例子:

  1. improt time
  2. def test():
  3. time.sleep(2)
  4. print("test is running!")
  5. def deco(func):
  6. start = time.time()
  7. func()
  8. stop = time.time()
  9. print(stop-start)
  10. deco(test)

在第9行,我们把test当作实参传递给形参func,即func=test。注意,这里传递的是地址,也就是此时func也指向了之前test所定义的那个函数体,可以说在deco()内部,func就是test。在第13行处,把函数名后面加上括号,就是对函数的调用。

嵌套函数

嵌套函数指的是在函数内部定义一个函数,而不是调用,如:

  1. def func1():
  2. def func2():
  3. pass

外部的我们叫外函数,内部的我们叫内函数。

此外:函数只能调用和它同级别以及上级的变量或函数。也就是说:里面的能调用和它缩进一样的和他外部的,而内部的是无法调用的。

针对上面的例子,如果想要统计程序运行时间,并且满足三原则,可以如下做:

  1. improt time
  2. def timer(func) #5
  3. def deco():
  4. start = time.time()
  5. func()
  6. stop = time.time()
  7. print(stop-start)
  8. return deco
  9. test = timer(test)
  10. def test():
  11. time.sleep(2)
  12. print("test is running!")
  13. test()

首先,在第11行,把test作为参数传递给了timer(),此时,在timer()内部,func = test,接下来,定义了一个deco()函数,并未调用只是在内存中保存了,并且标签为deco。在timer()函数的最后返回deco()的地址deco。然后再把deco赋值给了test,那么此时test已经不是原来的test了,也就是test原来的那些函数体的标签换掉了,换成了deco。那么在17行处调用的实际上是deco()。这段代码在本质上是修改了调用函数,但在表面上并未修改调用方式,而且实现了附加功能。


通俗一点的理解就是:把函数看成是盒子,test是小盒子deco是中盒子timer是大盒子。程序中,把小盒子test传递到大盒子temer中的中盒子deco,然后再把中盒子deco拿出来,打开看看(调用)。

这样做的原因是:我们要保留test(),还要统计时间,而test()只能调用一次(调用两次运行结果会改变,不满足),再根据函数即“变量”,那么就可以通过函数的方式来返回闭包。于是乎,就想到了把test传递到某个函数,而这个函数内恰巧内嵌了一个内函数,再根据内嵌函数的作用域(内嵌函数可以访问外部参数),把test包在这个内函数当中,一起返回,最后调用这个返回的函数。而test传递进入之后,再被包裹出来,显然test函数没有弄丢(在包裹里),那么外面剩下的这个test标签正好可以替代这个包裹。

闭包

闭包是Python编程一个非常重要的概念。如果一个外函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。

看个例子:

  1. def outer(x):
  2. a = x
  3. def inner(y):
  4. b = y
  5. print(a+b)
  6. return inner
  7. f1 = outer(1) # 返回inner函数对象+局部变量1(闭包)
  8. f1(10) # 相当于inner(10)。输出11

在这里,outer方法返回的不只是内函数对象,实际上是一个由inner函数和外部引用变量(a)组成的闭包!

一般一个函数运行结束的时候,临时变量会被销毁。但是闭包是一个特别的情况。当外函数发现,自己的临时变量会在将来的内函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。这就是闭包的强大之处。

装饰器用法

装饰器语法糖

根据以上分析,装饰器在装饰时,需要在每个函数前面加上:

  1. test = timer(test)

显然有些麻烦,Python提供了一种语法糖,即:

  1. @timer

这两句是等价的,只要在函数前加上这句,就可以实现装饰作用。以上为无参形式。

通用装饰器写法

来看一个简单的装饰器的例子:其作用是在某个函数运行前给我们提示。这里外函数以hint命名,内函数以常用的wrapper(包裹函数)命名。

  1. def hint(func):
  2. def wrapper(*args, **kwargs):
  3. print('{} is running'.format(func.__name__))
  4. return func(*args, **kwargs)
  5. return wrapper
  6. @hint
  7. def hello():
  8. print("Hello!")

我们现在对hello已经进行了装饰,当调用hello()时,可以看到如下结果。

  1. >>> hello()
  2. hello is running.
  3. Hello!

值得一提的是被装饰器装饰过的函数看上去名字没变,其实已经变了。当运行hello()后,会发现它的名字已经悄悄变成了wrapper,这显然不是我们想要的。不过这一点也不奇怪,因为外函数返回的是由wrapper函数和其外部引用变量组成的闭包。

  1. >>> hello.__name__
  2. 'wrapper'

为了解决这个问题保证装饰过的函数__name__属性不变,我们可以使用functools模块里的wraps方法,先对func变量进行wraps。下面这段代码可以作为编写一个通用装饰器的示范代码

  1. from functools import wraps
  2. def hint(func):
  3. @wraps(func)
  4. def wrapper(*args, **kwargs):
  5. print('{} is running'.format(func.__name__))
  6. return func(*args, **kwargs)
  7. return wrapper
  8. @hint
  9. def hello():
  10. print("Hello!")

这时,已经学会写一个比较通用的装饰器啦,并保证装饰过的函数__name__属性不变。当然使用嵌套函数也有缺点,比如不直观。这时你可以借助Python的decorator模块可以简化装饰器的编写和使用。如下所示。

  1. from decorator import decorator
  2. @decorator
  3. def hint(func, *args, **kwargs):
  4. print('{} is running'.format(func.__name__))
  5. return func(*args, **kwargs)

基于类实现的装饰器

Python的装饰器不仅可以用嵌套函数来编写,还可以使用类来编写。其调用__init__方法创建实例,传递参数,并调用__call__方法实现对被装饰函数功能的添加。

类的装饰器写法, 不带参数:

  1. from functools import wraps
  2. class Hint(object):
  3. def __init__(self, func):
  4. self.func = func
  5. def __call__(self, *args, **kwargs):
  6. print('{} is running'.format(self.func.__name__))
  7. return self.func(*args, **kwargs)

类的装饰器写法, 带参数:

  1. from functools import wraps
  2. class Hint(object):
  3. def __init__(self, coder=None):
  4. self.coder = coder
  5. def __call__(self, func):
  6. @wraps(func)
  7. def wrapper(*args, **kwargs):
  8. print('{} is running'.format(func.__name__))
  9. print('Coder: {}'.format(self.coder))
  10. return func(*args, **kwargs) # 正式调用主要处理函数
  11. return wrapper

装饰有参函数

先有如下代码:

  1. improt time
  2. def timer(func)
  3. def deco():
  4. start = time.time()
  5. func()
  6. stop = time.time()
  7. print(stop-start)
  8. return deco
  9. @timer
  10. def test(parameter):
  11. time.sleep(2)
  12. print("test is running!")
  13. test()

对于一个实际问题,往往是有参数的,如果要在第12行,给被修饰函数加上参数,显然这段程序会报错的。错误原因是test()在调用的时候缺少了一个位置参数的。而我们知道test = func = deco,因此test()=func()=deco(),那么当test(parameter)有参数时,就必须给func()deco()也加上参数,为了使程序更加有扩展性,因此在装饰器中的deco()func(),加如了可变参数*agrs**kwargs。完整代码如下:

  1. improt time
  2. def timer(func)
  3. def deco(*args, **kwargs):
  4. start = time.time()
  5. func(*args, **kwargs)
  6. stop = time.time()
  7. print(stop-start)
  8. return deco
  9. @timer
  10. def test(parameter):
  11. time.sleep(2)
  12. print("test is running!")
  13. test()

那么我们再考虑个问题,如果原函数test()的结果有返回值呢?比如:

  1. def test(parameter):
  2. time.sleep(2)
  3. print("test is running!")
  4. return "Returned value"

那么面对这样的函数,如果用上面的代码来装饰,最后一行的test()实际上调用的是deco()。有人可能会问,func()不就是test()么,怎么没返回值呢?其实是有返回值的,但是返回值返回到deco()的内部,而不是test()deco()的返回值,那么就需要再返回func()的值,因此就是:

  1. def timer(func)
  2. def deco(*args, **kwargs):
  3. start = time.time()
  4. res = func(*args, **kwargs)
  5. stop = time.time()
  6. print(stop-start)
  7. return res # 加入返回值
  8. return deco

完整程序为:

  1. improt time
  2. def timer(func)
  3. def deco(*args, **kwargs):
  4. start = time.time()
  5. res = func(*args, **kwargs)
  6. stop = time.time()
  7. print(stop - start)
  8. return res
  9. return deco
  10. @timer
  11. def test(parameter):
  12. time.sleep(2)
  13. print("test is running!")
  14. return "Returned value"
  15. test()

带参数的装饰器

若一个装饰器,对不同的函数有不同的装饰。那么就需要知道对哪个函数采取哪种装饰。因此,就需要装饰器带一个参数来标记一下。例如:

  1. @decorator(parameter = value)

举个例子,现在有两个函数:

  1. def task1():
  2. pass
  3. def task2():
  4. pass

要同时调用这两个函数,就要构造一个装饰器timer,并且需要告诉装饰器哪个是task1,哪个是task2,也就是要这样:

  1. @timer(parameter='task1')
  2. @timer(parameter='task2')

这也就是带参数的装饰器。

那么方法有了,但是我们需要考虑如何把这个parameter参数传递到装饰器中,我们以往的装饰器,都是传递函数名字进去,而这次,多了一个参数,要怎么做呢?于是,就想到再加一层函数来接受参数,根据嵌套函数的概念,要想执行内函数,就要先执行外函数,才能调用到内函数,那么就有:

  1. import time
  2. def timer(parameter):
  3. def outer_wrapper(func):
  4. def wrapper(*args, **kwargs):
  5. if parameter == 'task1':
  6. start = time.time()
  7. func(*args, **kwargs)
  8. stop = time.time()
  9. print("the task1 run time is :", stop - start)
  10. elif parameter == 'task2':
  11. start = time.time()
  12. func(*args, **kwargs)
  13. stop = time.time()
  14. print("the task2 run time is :", stop - start)
  15. return wrapper
  16. return outer_wrapper
  17. @timer(parameter='task1')
  18. def task1():
  19. time.sleep(2)
  20. print("in the task1")
  21. @timer(parameter='task2')
  22. def task2():
  23. time.sleep(2)
  24. print("in the task2")
  25. task1()
  26. task2()

首先timer(parameter),接收参数_parameter_='task1/2',而@timer(parameter)也恰巧带了括号,那么就会执行这个函数, 那么就是相当于:

  1. timer = timer(parameter)
  2. task1 = timer(task1)

后面的运行就和一般的装饰器一样了。

参考