目录
前言
在学习自动化测试框架pytest时,经常发现有很多类或者函数头上会带一顶@开头的帽子,如@pytest.fixture或者@pytest.mark.parametrize等。很多核心的用法(像参数化、fixture)都是通过@开头的帽子来实现的,看起来很高端的样子。放在一个函数开始定义的地方,像一顶帽子戴在函数头上,和函数绑定在一起,它就是python中的一个重难点:装饰器。
闭包
想要理解装饰器,必须先了解什么是闭包(closure)。
如果内部函数里引用了外部函数里定义的对象(甚至是外层之外,但不是全局变量),那么此时内部函数就被称为闭包函数。闭包函数所引用的外部定义的变量被叫做自由变量。闭包从语法上看非常简单,但是却有强大的作用。闭包可以将其自己的代码和作用域以及外部函数的作用结合在一起。
这段代码的意思:定义了一个外层函数outer
和一个内部函数inner
;在outer
函数内部,又定义了一个变量greetings
并赋值;然后在内部函数inner
中调用了这个变量;最后outer
函数的返回就是inner
函数本身。
def outer():
greetings = "hi "
def inner(name):
return greetings + name
return inner
print(outer()('dake'))
先执行outer()函数,来看看返回的结果:
可以看到outer
返回了一个函数,其实就是它的内层函数inner
,outer()("dake")
这一句其实就等同于inner("dake")
。
当我们Debug 调试去执行这个函数可以发现,进入outer
函数内部后,当执行到def inner(name):
这里后,并不会马上进入inner
函数的内部,而是先执行return inner
这句,然后才会开始进入inner
内部。虽然外层函数执行结束,但是内层函数inner
仍然可以调用outer
的局部变量greetings
!
闭包的特点:
- 函数中嵌套了另一个函数
- 外层函数的返回值是嵌套的内层函数
- 内层函数对外部函数有访问(引用了外部变量但非全局变量)
装饰器 其实就是闭包的一种引用,让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。
装饰器最大的作用就是对于我们已经写好的程序,我们可以抽离出一些雷同的代码组建多个特定功能的装饰器,这样我们就可以针对不同的需求去使用特定的装饰器,这时因为源码去除了大量泛化的内容而使得源码具有更加清晰的逻辑。
装饰器入门
例如:我要对一个加法函数,输出日志信息。
def sum():
sum = 0
logger.info("开始进行累加和")
for i in range(0, 101):
sum += i
logger.info("完成累加和")
return sum
但如果减法、乘法、其他场景也需要类似信息,那么多函数我都要一一去加上这么一长串东西吗?肯定不合适的,这时候就可以用装饰器来实现。Python为了让大家写起来方便,给装饰器提供了一个语法糖。直接在方法上戴一顶帽子就可以了。
# 外层函数
def outer(func):
# 内层被装饰的功能函数
def inner(total):
# 扩展的功能
logger.info("开始进行累加和")
# 功能函数
total = func(total)
sleep(1)
# 扩展的功能
logger.info("完成累加和")
return total
return inner
@outer
def sum(total):
for i in range(0, 101):
total += i
return total
print(sum(0))
运行结果:
2021-03-17 21:25:48.382 | INFO | __main__:inner:18 - 开始进行累加和
5050
2021-03-17 21:25:49.382 | INFO | __main__:inner:23 - 完成累加和
但是要装饰的函数只有3个、4个甚至有关键字参数呢,针对这个问题,python中给我们提供了不定长参数*args
和**kwargs**
# 外层函数
def outer(func):
# 内层被装饰的功能函数
def inner(*args, **kwargs):
# 扩展的功能
logger.info("开始进行加法计算")
# 功能函数
total = func(*args, **kwargs)
sleep(1)
# 扩展的功能
logger.info("完成加法计算")
return total
return inner
@outer
def add(a, b):
return a + b
运行结果:
2021-03-17 21:35:34.730 | INFO | __main__:inner:18 - 开始进行累加和
3
2021-03-17 21:35:35.740 | INFO | __main__:inner:23 - 完成累加和
带参数的装饰器
如果我想通过装饰器给功能函数传递参数,该怎么办呢?
我们首先要接收这个传入的参数,需要在原来的装饰器外面再加一层用来接收外部参数。
# 加了一层用来接收外部参数
def add_name(name):
def outer(func):
def inner(*args, **kwargs):
print(f'{name}正在调用该函数')
return func(*args, **kwargs)
return inner
# 返回第二层函数
return outer
@add_name('dake')
def add(a, b):
res = a + b
return res
print(add(1, 2))
我们把原来的装饰器outer
看做一个整体,即一个内层函数,add_name
就是它的外层函数,外层作用域中有一个变量name
传入。上面闭包中讲过,内层是可以引用外部作用域的变量的,因此,在最里层的功能函数,就可以直接引用这个name
。于是,通过再嵌套一层,实现了新增的参数。
原函数信息似乎变了
控制台打印add原信息,发现:原函数的信息似乎变了
print(add.__name__)
# 输出:inner
help(add)
# 输出:Help on function inner in module __main__:
# inner(*args, **kwargs)
我们发现,add()
函数被装饰以后,它的元函数信息变了,不再是以前那个add()函数,而是被inner()取代了。这是装饰器带来的一个副作用,会覆盖掉原函数的元信息。
为了解决这问题,可以通过内置的装饰器@functools.wrap
解决,它能帮助保留原函数的元信息。
import functools
def add_name(name):
def outer(func):
@functools.wraps(func)
def inner(*args, **kwargs):
print(f'{name}正在调用该函数')
return func(*args, **kwargs)
return inner
# 返回第二层函数
return outer
@add_name('dake')
def add(a, b):
res = a + b
return res
print(add.__name__)
help(add)
# 输出:add
# Help on function add in module __main__:
# add(a, b)