python的闭包、装饰器和functools.wraps

1. 闭包

1.1 作用域

作用域的概念就是变量可以被感知的空间范围。

LEGB法则: Python会按照优先级依次搜索4个作用域,以此来确定该变量名的意义。首先搜索局部作用域(L),之后是上一层嵌套结构中def或lambda函数的嵌套作用域(E),之后是全局作用域(G),最后是内置作用域(B)。按这个查找原则,在第一处找到的地方停止。如果没有找到,则会出发NameError错误。

引用这篇博客的资料:

Python中并不是所有的语句块中都会产生作用域。只有当变量在Module(模块)、Class(类)、def(函数)中定义的时候,才会有作用域的概念。在作用域中定义的变量,一般只在作用域中有效。 需要注意的是:在if-elif-else、for-else、while、try-except\try-finally等关键字的语句块中并不会产成作用域。看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 产生作用域
def func():
variable = 100
print variable
print variable

# NameError: name 'variable' is not defined
# 2. 不产生作用域
if True:
variable = 100
print (variable)
print ("******")
print (variable)

#100
#******
#100

1.2 闭包

闭包,个人理解是将一些变量打包进了函数中,然后这个函数生成之后供外部调用,打包进去的变量脱离了作用域的限制,和这个闭包函数共存。关于闭包的详情可以看这篇博客

闭包的概念:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

例:

1
2
3
4
5
6
7
8
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}

执行情况:

1
2
3
>>> var f = lazy_sum([1, 2, 3, 4, 5])
>>> f()
15

1.3 闭包陷阱:

闭包创建后返回之时,其内部的自由变量是确定的值

有一个闭包陷阱的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
def my_func(*args):
fs = []
for i in xrange(3):
def func():
return i * i
fs.append(func)
return fs

fs1, fs2, fs3 = my_func()
print fs1()
print fs2()
print fs3()

返回闭包列表fs之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数。 当然这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个local variable。

执行结果:

1
2
3
4
4
4

经过上面的分析,我们得出下面一个重要的经验:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。 这条规则本质上是在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化

正确的改写:

1
2
3
4
5
6
7
def my_func(*args):
fs = []
for i in xrange(3):
def func(_i = i): # 传给闭包的是一个具体值
return _i * _i
fs.append(func)
return fs

2. 装饰器

何谓“装饰器”? 《A Byte of Python》中这样讲: “Decorators are a shortcut to applying wrapper functions. This is helpful to “wrap” functionality with the same code over and over again.”
《Python参考手册(第4版)》6.5节描述如下: “装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。”
Python官方文档中这样定义: “A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod().”

总的来说,装饰器(wrapper)是一个函数的工厂,他接收一个函数参数,返回的也是一个函数。一般其作用是将原有函数添加上新的一些功能,然后将新的功能更强的函数返回。 这里有一个打印函数执行时间的装饰器time_cost

1
2
3
4
5
6
7
8
9
10
import time

def time_cost(func):
def wrapper(*args, **kwargs):
s = time.time()
func(*args, **kwargs)
cost = (time.time() - s) * 1000
print('time cost: {} ms'.format(int(cost)))
return
return wrapper

可以看出time_cost这个装饰器做了这么三件事: 1. 将传参func带进闭包函数wrapper中; 2. 闭包中将传递过来的func带上其原有参数*args**kwargs执行,然后用time模块统计其执行的时间; 3. 将闭包wrapper返回

2.1 语法糖@

被语法糖@装饰的函数,调用的时候等同于:

1
wrapper(func)()

如果不用语法糖,也可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
def func_wrapper(func):
def wrapper(*args, **kwargs):
# ···
func(*args, **kwargs) # or res = func(*args, **kwargs)
# ···
retrun # return anything like func does

def _func_name(*args, **kwargs):
# ···
return

func_name = func_wrapper(_func_name) # func_name具有原函数_func_name的功能,而且还经过了func_wrapper装饰,因此它拥有更多的功能
···

2.2 带参数的装饰器

如果装饰器本身带有参数(如需判断逻辑or其他作用),则需要再包一层,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 装饰器本身带有参数
def human(sex: str):
def decorator(func):
def wrapper():
if sex == 'm':
print('i am male')
if sex == 'f':
print('i am female')
func()
return
return wrapper
return decorator

# 使用human装饰器
@human(sex='m')
def man():
print('in function man()')

@human(sex='f')
def women():
print('in function women()')

执行情况:

1
2
3
4
5
6
>>> man()
i am male
in function man()
>>> women()
i am female
in function women()

2.3 装饰带有参数的函数

其实在前面的例子中,已经有涉及了装饰带有参数的函数。实际上就是被装饰的函数本身是有自身参数的,为了使得wrapper通用,一般采取*args和**kwargs的方式传递参数,我们将前例改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 装饰器本身带有参数
def human(sex: str):
def decorator(func):
def wrapper(*args, **kwargs): # 两层的闭包,传递进来sex和func
if sex == 'm':
print('i am male')
if sex == 'f':
print('i am female')
func(*args, **kwargs)
return
return wrapper
return decorator

# 使用human装饰器
@human(sex='m')
def man(name):
print('in function man(), my name is {}'.format(name))

@human(sex='f')
def women(name):
print('in function women(), my name is {}'.format(name))

执行情况:

1
2
3
4
5
6
>>> man('Tom')
i am male
in function man(), my name is Tom
>>> women('Lucy')
i am female
in function women(), my name is Lucy

3. functools

关于functools里的wraps,个人理解的作用就是:装饰后返回的闭包,如果查看其__doc__, __name__等属性,获取到的是wrapper本身的值,用了functools.wraps装饰后,他能将这个返回的闭包的属性全体更新一遍,后面的人再次查看,看到的就是wrapped函数中的东西了。


分割线,下面全是阅读的文章的引用:

  1. functools.wraps 旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,已达到装饰器不修改原函数的目的。
  2. wraps内部通过partial对象和update_wrapper函数实现。
  3. partial是一个类,通过实现了双下方法new,自定义实例化对象过程,使得对象内部保留原函数和固定参数,通过实现双下方法call,使得对象可以像函数一样被调用,再通过内部保留的原函数和固定参数以及传入的其它参数进行原函数调用。

这篇博客中详细讲述了update_wrapper和partial的作用:update_wrapper做的工作很简单,就是用参数wrapped表示的函数对象(例如:square)的一些属性(如:namedoc)覆盖参数wrapper表示的函数对象(例如:callf,这里callf只是简单地调用square函数,因此可以说callfsquare的一个wrapper function)的这些相应属性。
因此,本例中使用wraps装饰器“装饰”过callf后,callf的docname等属性和trace要“装饰”的函数square的这些属性完全一样。
经过上面的分析,相信你也了解了functools.wraps的作用了吧。

以下篇幅引用自stackoverflow的答案
When you use a decorator, you’ve wrapped your original code in another function, making the original function invisible. To continue your example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def strong(func):      
def wrapper():
return '<strong>' + func() + '</strong>'
return wrapper

@strong
def greet():
return 'Hello!'

def weak_greet():
return 'hi'

print(greet)
print(weak_greet)

If you run this, you get the following output. .wrapper at 0x000000000129A268>
When you used the decorator, you took your function, created a new function that wrapped code around your old function and returned that new, anonymous, function. (当你用装饰器时,实际上就是用一个 新的匿名的 function去 包装 传递过来的function,而这个新的匿名的function,他的工作就是加入一些其他的代码将原function wrap起来。)

接下去是functools.wraps的作用:

You can see some unpleasant effects if you try to pickle it.
if you do pickle.dumps(weak_greet), you get b'\x80\x03c__main__\nweak_great\nq\x00.'. but if you try to pickle.dumps(greet), you get AttributeError: Can't pickle local object 'strong..wrapper'. (dealing with decorated classes and functions that must be pickled is one of the circles of hell I don’t wish to revisit any time soon).
You are not adding to your function. You are wrapping your original function in a shiny new function. That new function says, “There’s something I’m hiding in here and I won’t tell you what it is (functools.wraps can sometimes help with this, as it would in your case). But, when you give me input, I’ll alter it like so (or not at all), pass it to my secret function, (possibly) alter the output and give you that. Your original function is inaccessible (hence pickle’s confusion).
NOTE: You can re-create the look of your original function by further wrapping your wrapper with `@functools.wraps(original_function)`, which does not affect output, but wraps everything in a box to make it look exactly like the original function. so,

1
2
3
4
5
6
from functools import wraps  
def strong(func):
@wraps(func)
def wrapper():
return '<strong>' + func() + '</strong>'
return wrapper

would now look like your original function and be pickle-able. It would be like wrapping a surprise present, and then wrapping the present again with wrapping paper that told you (in great detail) what the surprise was.

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!