浅析python函数闭包和装饰器
一,闭包
闭包这个特性我想大家都有所耳闻,但是何为闭包呢?请听我一一道来。
要理解闭包,首先需要理解的是"变量作用域"。先来看一段代码
>>> def f1(a): ... print(a) ... print(b) ... >>> f1(5) 5 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in f1 NameError: global name 'b' is not defined
这个f1函数很简单,就是接受一个参数a,打印a和b。运行函数,报NameError错误,这并不奇怪,因为变量"b"没有定义。那么当我们定义b之后,f1就能正常执行了。
>>> b = 2
>>> f1(5)
5
2
ok,一切都很正常!但是,任何事情都有可能会发生意外,比如下面这段代码。
>>> b = 2 >>> def f2(a): ... print(a) ... print(b) ... b = 9 ... >>> f2(5) 5 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in f2 UnboundLocalError: local variable 'b' referenced before assignment
what??? why??? 一脸懵逼有没有?我明明定义了变量"b", 但是为什么还是报错了??
事实上, python编译函数的定义体时, 它把b判定为函数f2的局部变量了,因为在f2中有一句 "b=2" 这样的赋值语句,所以在函数的第三行 print(b),显然会报错,因为这个时候你还没有定 义局部变量"b" 。那么这个问题如何解决呢? 其实只要加一条语句就可以,下面请看。
>>> b = 2 >>> def f3(a): ... global b ... print(a) ... print(b) ... b = 9 ... >>> f3(5) 5 2 >>> b 9 >>> f3(6) 6 9 >>> b 9
添加的这一条语句是 "global b", 这条语句的意思是把b当成全局变量。而我们刚好也定义了全局变量b,所以运行f3函数是不会报错的,运行完f3函数,可以发现全局变量b变成了9,这是 因为在f3函数中,全局变量b被修改了(b=9). OK, 接下来可以讨论闭包了。
定义:闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
乍一看闭包的定义,我相信很多人是不能 理解的,包括我自己:这定义是什么意思?很抽象有木有?为了能理解闭包(closure) 先来看一段代码。
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager
该函数其实不复杂,是用来计算移动平均值(比如股票均线的计算)的。 在make_averager函数中定义了一个局部变量series, 这很好理解。 接下来我们来调用这个函数
>>> def make_averager(): ... series = [] ... def averager(new_value): ... series.append(new_value) ... total = sum(series) ... return total/len(series) ... return averager ... >>> avg = make_averager() >>> avg(5) 5 >>> avg(6) 5 >>> avg(12) 7
当调用avg(5)时,make_averager函数已经返回了,那么他的本地作用域也就一去不复返了。而在averager函数中,series是自由变量(free variable)。 这个术语的意思是,未在本地作用域 绑定的变量,参见下图。

averager的闭包延伸到哪个函数的作用域之外,包含自由变量series的保定。
这下大家应该都理解了吧? OK, 关于闭包就讨论到这里,接下来讲装饰器。
二、装饰器
先来看一段代码
def wraper(args): print("in function wraper") return args def f1(): print("in function f1") f1 = wraper(f1) f1()
运行结果:
in function wraper in function f1
这段代码是把函数f1当做参数传给wraper函数并赋值给f1,因为wraper函数返回的是参数args,所以当进行赋值"f1 = wraper(f1)" 其实是执行了wraper函数,并且返回原f1函数对象(因为这 个赋值语句的参数args是f1),最后赋值给f1,此时f1其实是wraper函数返回的对象。接下来执行语句"f1()" , 也就是说这时候调用f1(), 其实是调用了wraper返回对象的__call__(函数都是可 调用对象,都有这个方法)方法。
这其实已经实现了一个简单的装饰器。这段代码其实等价于
def wraper(args): print("in function wraper") return args @wraper def f1(): print("in function f1") f1()
在python中,@是一个黑魔法,它的作用是把被装饰函数(下方的那个函数,在这里是f1)当做参数传递给装饰器函数(在这里是wraper)并执行。执行wraper并且把返回值复制给新的f1,注意这里的wraper函数返回值并不是f1(),而是f1。接下来执行f1()语句,其实这时候的f1已经不是原来的那个f1函数了,它已经被wraper"装饰"过了。那么这时候调用"f1()"其实相当于执行了新的f1(被wraper装饰过的f1)。 在这个例子中,可能新的f1和原来老的f1并没有什么差别,那么接下来再来看一个例子。
import time def clock(func): def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ arg_str = ', '.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked @clock def f1(args): print("f1 is executed with args:%s" % args) time.sleep(0.5) return "hello %s" % args f1("world")
运行结果
f1 is executed with args:world
[0.49959273s] f1('world') -> 'hello world'
在这个例子中,f1被装饰器clock装饰。也就是说,当应用"@clock" 这个"黑魔法" 语句在函数f1上,其实相当于:把f1当做参数传递给装饰器函数(clock)并执行,执行后的返回值重新赋值给f1。 如果不用@clock这样的语句,相当于以下代码
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
# @clock
# def f1(args):
# print("f1 is executed with args:%s" % args)
# time.sleep(0.5)
# return "hello %s" % args
#
# f1("world")
def f1(args):
print("f1 is executed with args:%s" % args)
time.sleep(0.5)
return "hello %s" % args
f1 = clock(f1)
ret = f1("world")
print(ret)
执行clock函数(参数是f1), 返回值是clocked函数,注意是clocked函数, 而不是clocked() 。也就是说此时f1 = clocked 。而clocked函数定义如下。
def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ arg_str = ', '.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result
那么接下来执行f1("world") 其实就相当于执行了clocked("world"), 最终得到返回值,并打印。运行结果如下
f1 is executed with args:world [0.50013607s] f1('world') -> 'hello world' hello world
OK, 我相信这回大家明白装饰器是怎么回事了吧?
在这里我们的f1函数(被装饰函数)只有一个参数,有同学可能要问,如果f1是多个参数的函数怎么办? 或者,f1包含关键字参数怎么办? 没关系,只要我们把刚才的装饰器稍微改动下,就能实现了。请看例子
import time def clock(func): def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - t0 name = func.__name__ arg_str = ', '.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked @clock def f1(args): print("f1 is executed with args:%s" % args) time.sleep(0.5) return "hello %s" % args @clock def f2(name, age=20): print("Hello all, My name is %s, and I'm %s" % (name, age)) time.sleep(1) return "hello %s" % name ret1 = f1("world") ret2 = f2("Ethan", age=25) print(ret1) print(ret2)
运行结果 f1 is executed with args:world [0.49919400s] f1('world') -> 'hello world' Hello all, My name is Ethan, and I'm 25 [0.99994619s] f2('Ethan') -> 'hello Ethan' hello world hello Ethan
其实就是将装饰器的内部函数定义为 clocked(*args, **kwargs) 这种格式就行。如果不明白* 和** 的用法的同学,可以去看看python函数参数相关内容,这里就不做介绍了。OK,关于装饰器我就讲到这里,有兴趣的同学可以去了解下python标准包中自带的装饰器。如下
from functools import wraps from functools import lru_cache from functools import singledispatch
posted on 2018-03-17 15:38 hz_pythoner 阅读(125) 评论(0) 收藏 举报
浙公网安备 33010602011771号