python闭包、装饰器初使用

1、闭包

看网上有很多种闭包讲法:

(1)

简单举一个例子:

def foo():
    m=3
    n=5
    def bar():
        a = 4
        return m+n+a
    return bar

>>> bar = foo()
>>> bar()
12

简单的说,这种内部函数可以使用外部函数变量的行为,就叫闭包。

(2)闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

一个闭包就是你调用了一个函数A,这个函数A返回了一个函数B给你。这个返回的函数B就叫做闭包。你在调用函数A的时候传递的参数就是自由变量。

举个例子:

def func(name):
    def inner_func(age):
        print 'name:', name, 'age:', age
    return inner_func

bb = func('the5fire')
bb(26)  # >>> name: the5fire age: 26

这里面调用func的时候就产生了一个闭包--inner_func,并且该闭包持有自由变量--name,因此这也意味着,当函数func的生命周期结束之后,name这个变量依然存在,因为它被闭包引用了,所以不会被回收。

(3)所谓闭包,就是将组成函数的语句和这些语句的执行环境打包在一起时,得到的对象。

听上去的确有些复杂,还是再用一个栗子来帮助理解一下深层意思。假设我们在foo.py模块中做了如下定义:

#foo.py
filename = "foo.py"

def call_func(f):
    return f()    # f引用一个函数对象,然后调用它

在另一个func.py模块中,写下了这样的代码:

#func.py
import foo      #导入foo.py

filename = "func.py"
def show_filename():
    return "filename: %s" % filename

if __name__ == "__main__":
    print foo.call_func(show_filename)   #注意:实际发生调用的位置,是在foo.call_func函数中
# 输出:filename:func.py

很显然show_filename()函数使用的filename变量的值,是在与它相同环境(func.py模块)中定义的那个。尽管foo.py模块中也定义了同名的filename变量,而且实际调用show_filename的位置也是在foo.py的call_func内部

 

而对于嵌套函数,这一机制则会表现的更加明显:闭包将会捕捉内层函数执行所需的整个环境

#enclosed.py
import foo
def wrapper():
    filename = "enclosed.py"
    def show_filename():
        return "filename: %s" % filename
    print foo.call_func(show_filename)    #输出:filename: enclosed.py

实际上,每一个函数对象,都有一个指向了该函数定义时所在全局名称空间的__globals__属性:

#show_filename inside wrapper
#show_filename.__globals__

{
'__builtins__': <module '__builtin__' (built-in)>,        #内建作用域环境
'__file__': 'enclosed.py',         
'wrapper': <function wrapper at 0x6f84776b6274>,      #直接外围环境
'__package__': None,            
'__name__': '__main__',        
'foo': <module 'foo' from './foo.pyc'>,         #全局环境
'__doc__': None                   
}

当代码执行到show_filename中的return "filename: %s" % filename语句时,解析器按照下面的顺序查找filename变量:

  • Local - 本地函数(show_filename)内部,通过任何方式赋值的,而且没有被global关键字声明为全局变量的filename变量;
  • Enclosing - 直接外围空间(上层函数wrapper)的本地作用域,查找filename变量(如果有多层嵌套,则由内而外逐层查找,直至最外层的函数);
  • Global - 全局空间(模块enclosed.py),在模块顶层赋值的filename变量;
  • Builtin - 内置模块(__builtin__)中预定义的变量名中查找filename变量;

在任何一层先找到了符合要求的filename变量,则不再向更外层查找。如果直到Builtin层仍然没有找到符合要求的变量,则抛出NameError异常。这就是变量名解析的:LEGB法则。

 

总结:

闭包最重要的使用价值在于:封存函数执行的上下文环境

闭包在其捕捉的执行环境(def语句块所在上下文)中,也遵循LEGB规则逐层查找,直至找到符合要求的变量,或者抛出异常。

 

 

 

 

2、装饰器

“装饰器的功能是将被装饰的函数当作参数传递给与装饰器对应的函数(名称相同的函数),并返回包装后的被装饰的函数”

看一个例子:

先导入包

from functools import reduce
import math
import logging
logging.basicConfig(level=logging.INFO)

定义一个检查参数的装饰器:

def checkParams(fn):
    def wrapper(*numbers):
        temp = map(lambda x:isinstance(x,(int,)),numbers) # 检查参数是否都为整型
        if reduce(lambda x,y: x and y, temp): # 若都为整型
            return fn(*numbers)             # 则调用fn(*numbers)返回计算结果
        #否则通过logging记录错误信息,并友好退出
        logging.warning("variable numbers cannot be added")
        return
    return wrapper     #fn引用add,被封存在闭包的执行环境中返回

然后定义求最大公约数的函数(能求多个的最大公约数):

def gcd(*numbers):
    """return the greatest common divisor of the given integers."""
    return reduce(math.gcd, numbers)

调用:

>>>gcd = checkParams(gcd)      
>>>gcd(3, 'hello')
# 输出 WARNING:root:variable numbers cannot be added

注意checkParams函数:

  • 首先看参数fn,当我们调用checkParams(gcd)的时候,它将成为函数对象gcd的一个本地(Local)引用;
  • 在checkParams内部,我们定义了一个wrapper函数,添加了参数类型检查的功能,然后调用了fn(*numbers),根据LEGB法则,解释器将搜索几个作用域,并最终在(Enclosing层) checkParams函数的本地作用域中找到fn;
  • 注意最后的return wrapper,这将创建一个闭包,fn变量(gcd函数对象的一个引用)将会封存在闭包的执行环境中,不会随着checkParams的返回而被回收;

当调用gcd = checkParams(gcd)时,gcd指向了新的wrapper对象,它添加了参数检查和记录日志的功能,同时又能够通过封存的fn,继续调用原始的gcd进行最大公约数运算。

因此调用gcd(3, 'hello')将不会返回计算结果,而是打印出日志:root:variable numbers cannot be added。

 

有人觉得add = checkParams(add)这样的写法未免太过麻烦,于是python提供了一种更优雅的写法,被称为语法糖

@checkParams
def lcm(*numbers):
    """return lowest common multiple."""
    f = lambda a,b:int((a*b)/gcd(a,b))
    return reduce(f, numbers)

这只是一种写法上的优化,解释器仍然会将它转化为gcd = checkParams(gcd)来执行。

 

就像下图:其中a为 与装饰器@a对应的函数, b 为装饰器修饰的函数,装饰器@a的作用是:

简而言之:@a 就是将 b 传递给 a(),并返回新的 b = a(b)

 

posted @ 2018-03-17 17:32  chen狗蛋儿  阅读(177)  评论(0)    收藏  举报