Python装饰器
听过Python的人,肯定也听过装饰器的名头,但是好多人不明白装饰器是什么,是如何工作的,原理又是什么。
先看看装饰器的定义:如果想要修改某个函数的功能,但是又不想修改这个函数的定义,这种在函数运行期间动态增加功能的方式成为装饰器。
简而言之,就是Python提供了一种手段,用于在函数运行期间扩充函数功能,这就是装饰器。
闭包
在了解装饰器之前,先来看看什么是闭包。
闭包就是内部函数对外部函数作用域里变量的引用。
这里的内部函数和外部函数又是什么鬼?每种编程语言都有作用域的概念,在一个函数定义了一个变量,在函数外部或者说是函数执行完成之后是不能引用这个变量的。在C++种,可以用函数栈来清楚的解释。在面向对象的Python中,一切皆对象,那么函数也可以理解为一个对象,函数名只是一个指向函数对象的变量,所以在函数中再定义一个函数也不是不可以,当然,作为一个C++的使用者,这种做法刚开始接触肯定会有一些不适应。下面用代码来直观的解释。
def func():
print('this is func')
def func1():
print('this is func1')
func()
直接去执行func,只会得到输出this is func
,但是函数func1在func内部定义,执行func不应该执行func1吗?
要理解这个就要说明一下,Python是一个面向对象的语言,其所有的一切都可以理解为对象,在func内部定义了func1,只是定义了一个函数对象,并没有像func()
显式执行它,所以不会有this is func1
的输出
那么现在如果想要执行func1,并且不能改变它定义的位置,该如何去做。
可以将func1这个函数对象返回,在外部调用他
def func():
print('this is func')
def func1():
print('this is func1')
return func1
var = func()
var()
来看看执行结果
正如我们所预料的,func1在func内部定义,在func执行完毕之后将其返回,var指向了func1,此时可以理解为var == func1
,调用var就相当于调用func1,如果要验证也是很简单,将他们的内存地址打印出来即可。
接着来说闭包,闭包是内部函数对外部函数变量的引用,那么内部函数是如何引用的,还是拿上面的列子来说,在func中定义一个变量a,在func1中使用这个a会如何
def func():
a = 1
def func1(num):
print(num + a)
return func1
var = func()
var(3)
可以看到结果是4,证明在func1这个内部函数中确实保留了外部函数func的变量,那么如何验证这两个变量是同一个对象呢,很简单,将其内存地址打印出来即可。
def func():
a = 1
print(id(a))
def func1(num):
print(id(a))
print(num + a)
return func1
var = func()
var(3)
可以证实在func1中确实保留了func中的变量,但是了解Python的变量内存机制的同学,会很快想到在内存中只有一个1,只是func中的a指向了它而已,在正常的情况下,func函数运行完毕,其内部维护的变量a也会消亡,也就是内存中的1会被释放,但是由于内部函数还在引用,所以内存中的1并不会很快消亡,如果想要彻底释放可以使用del var
来释放被引用的变量。这就是一个完整的闭包。
ps:如果想要了解Python的内存机制,可以看这篇文章
再来看一个比较清晰的例子来理解闭包:
my_list = [1, 2, 3, 4, 5]
def func(obj):
obj[1] += 10
print('in func:', obj)
def func_in():
obj[0] += 1
print('in func_in:', obj)
return func_in
outter = func(my_list)
outter()
print(my_list)
这里将一个列表作为参数传入外部函数,内部函数使用这个列表进而影响到外部列表。函数装饰器是基于函数闭包来实现的,只不过区别就是外部函数接受的参数是一个函数。这也是函数式编程的一个特点,可以接受一个函数作为参数,也可以返回一个函数。
装饰器
回到装饰器的概念上来,它是为了在函数运行期间添加一些其他的功能,另一方面,装饰器又是基于函数闭包来实现的,所以,我们现在就可以写出一个简单的装饰器。
我们现在有一个简单的打印时间的函数
def now():
print(datetime.now())
但是我们在不想改变这个函数功能的情况下要打印出正在执行的函数的名字,要实现这个功能就要实现一个简单的装饰器。
def log(func):
def wrapper():
print('call %s()' % func.__name__)
return func()
return wrapper
这就是最简单的一个装饰器,外部函数log接受一个func,是一个被装饰的函数对象,内部函数返回了被装饰函数的调用。这样一个装饰器就写好了,但是要去调用它的话就需要@
的帮助。
@log
def now():
print(datetime.now())
这样就实现了我们想要的功能,先看看执行后的结果。
这里将log
放在了now
函数的定义处,就改变了now本身的运行逻辑。之后在调用now时,其实就是相当于调用wrapper() + func() == now()
,更进一步的说,执行now就相当于执行now = log(now)
,要理解这个装饰过程,就要时刻牢记函数名只是一个指向函数对象的变量而已。
将原有函数now
作为参数传入log
中,作为形参名的func
和传入的now
是指向的同一个函数,而在log
内进行加工,将原有的函数放在wrapper
内执行,只不过在执行原有函数之前打印了一句话,这样就实现了函数动态运行需要添加的功能,而最后返回时,now
已经不是指向原来的函数,而是指向了wrapper
,而在wrapper
中,又执行了原来的函数。
可以用下面的图来加深理解:
可以看到,装饰器给我们了一种错觉,表面上执行的是now,但是实际上执行的是wrapper,在wrapper中执行了原本的now。这就是装饰器。
总的来说,装饰器需要接受一个被装饰的函数作为参数,并且还要进行一次调用。log(now)() == wrapper()
但是由于函数也是一个对象,它本身内部也有一些属性,比如__name__
等,在使用装饰器达到预期效果时,now看起来是now,但是now只是一个指向wrapper函数的变量,本质上还是wrapper函数,打印now.__name__
就可以看到。所以为了避免有些需要使用到函数属性发生错误的情况,Python本身也给我们提供了一个装饰器来解决这个问题。下面就是一个比较完整的装饰器的写法
def log(func):
@functools.wraps(func)
def wrapper():
print('this is decorator')
return func()
return wrapper
带参数的装饰器
上面认识了最简单的装饰器,那么如果我们有别的需求,需要在调用装饰器的时候给装饰器传入参数。拿上面的例子来说,我们在输出func.__name__
的同时需要输出一个自定义的文本,那上面的装饰器不能满足我们的需求,如何设计一个新的装饰器来接受我们的参数?
我们知道,闭包可以引用外部函数的变量,那么如果要接受参数,就需要创建一个新的函数来接受外部传入的参数。
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s()' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
这个装饰器的调用过程是这样的
@log('execute')
def now():
print(datetime.now())
和两层嵌套相比,三层嵌套的结果是这样的now = log('execute')(now)
执行log('execute')
返回的是decorator
函数,再调用这个函数,将now作为参数传入,返回wrapper
,可以达到装饰器的效果。