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 | # 1. 产生作用域 |
1.2 闭包
闭包,个人理解是将一些变量打包进了函数中,然后这个函数生成之后供外部调用,打包进去的变量脱离了作用域的限制,和这个闭包函数共存。关于闭包的详情可以看这篇博客。
闭包的概念:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
例:
1 | function lazy_sum(arr) { |
执行情况:
1 | >>> var f = lazy_sum([1, 2, 3, 4, 5]) |
1.3 闭包陷阱:
闭包创建后返回之时,其内部的自由变量是确定的值。
有一个闭包陷阱的例子如下:
1 | def my_func(*args): |
返回闭包列表fs之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数。 当然这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个local variable。
执行结果:
1 | 4 |
经过上面的分析,我们得出下面一个重要的经验:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。 这条规则本质上是在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化。
正确的改写:
1 | def my_func(*args): |
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 | import time |
可以看出time_cost
这个装饰器做了这么三件事: 1. 将传参func
带进闭包函数wrapper
中; 2. 闭包中将传递过来的func
带上其原有参数*args
和**kwargs
执行,然后用time模块统计其执行的时间; 3. 将闭包wrapper
返回
2.1 语法糖@
被语法糖@装饰的函数,调用的时候等同于:
1 | wrapper(func)() |
如果不用语法糖,也可以这么写:
1 | def func_wrapper(func): |
2.2 带参数的装饰器
如果装饰器本身带有参数(如需判断逻辑or其他作用),则需要再包一层,例:
1 | # 装饰器本身带有参数 |
执行情况:
1 | >>> man() |
2.3 装饰带有参数的函数
其实在前面的例子中,已经有涉及了装饰带有参数的函数。实际上就是被装饰的函数本身是有自身参数的,为了使得wrapper通用,一般采取*args和**kwargs的方式传递参数,我们将前例改写一下:
1 | # 装饰器本身带有参数 |
执行情况:
1 | >>> man('Tom') |
3. functools
关于functools
里的wraps
,个人理解的作用就是:装饰后返回的闭包,如果查看其__doc__
, __name__
等属性,获取到的是wrapper
本身的值,用了functools.wraps
装饰后,他能将这个返回的闭包的属性全体更新一遍,后面的人再次查看,看到的就是wrapped
函数中的东西了。
分割线,下面全是阅读的文章的引用:
- 知乎有个文章总结了
functools.wraps
的用法:
functools.wraps
旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,已达到装饰器不修改原函数的目的。wraps
内部通过partial
对象和update_wrapper
函数实现。partial
是一个类,通过实现了双下方法new
,自定义实例化对象过程,使得对象内部保留原函数和固定参数,通过实现双下方法call
,使得对象可以像函数一样被调用,再通过内部保留的原函数和固定参数以及传入的其它参数进行原函数调用。
这篇博客中详细讲述了update_wrapper和partial的作用:update_wrapper
做的工作很简单,就是用参数wrapped
表示的函数对象(例如:square
)的一些属性(如:name、 doc)覆盖参数wrapper
表示的函数对象(例如:callf
,这里callf
只是简单地调用square
函数,因此可以说callf
是 square
的一个wrapper function)的这些相应属性。
因此,本例中使用wraps装饰器“装饰”过callf后,callf的doc、name等属性和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 | def strong(func): |
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 dopickle.dumps(weak_greet)
, you getb'\x80\x03c__main__\nweak_great\nq\x00.'
. but if you try topickle.dumps(greet)
, you getAttributeError: 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 | from functools import wraps |
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.