python 中的闭包与装饰器

Posted by Charlie Lin on June 23, 2020

python 中的闭包与装饰器

闭包

何为闭包

简单介绍一下闭包的概念。 首先是闭包的定义,摘自维基百科

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)

通俗地讲:

在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。 词法作用域和函数的值传递。内部函数可以访问函数外的变量。 函数当作值传递,即所谓的first class对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。 作者:寸志 链接:https://www.zhihu.com/question/34210214/answer/93590294 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

再通俗一点:

闭包不是私有,闭的意思不是“封闭内部状态”,而是“封闭外部状态”。当函数外部状态的作用于失效的时候,还有一份保留在函数内部状态里。

闭包的最大特点是可以将外部函数的变量与内部函数绑定,并返回绑定变量后的内部函数(也即闭包),此时即便生成闭包的环境(外部函数执行时的环境)已经释放,闭包仍然存在。

要理解闭包,需要理解以下三个关键点:

  1. 函数
  2. 自由变量
  3. 环境

那么如何理解呢?举个例子:

shadowed_var = 10

def outer(x):
    shadowed_var = 20              #  <--+
    def inner(y):                  #     |
        tripple = 0                #     |
        tripple = shadowed_var * 3 #-----+
        return x + y + tripple
    return inner

fun = outer(10)
fun(20)

这个例子中变量shadowed_var不属于函数inner,因而在函数innershadowed_var属于自由变量。而inner捕获了自由变量shadowed_var,形成了闭包。 可以这么理解,一个函数中出现的所有变量,函数的参数以及函数中定义的局部变量称为“约束变量(非自由变量)”,而其他的变量则是自由变量。 在不允许函数嵌套的语言中,由于没有函数嵌套,自由变量就是全局变量。而像 python 这种函数是一等公民的语言,自由变量还可以是上一级函数中定义的局部变量(如上一个例子中的变量 shadowed_var)。 同时,在这个例子中,inner 中的 shadowed_var指向的是 outer 中定义的那个,而非全局变量的 shadowed_var。 而闭包的概念要求我们,如果一个函数创建时,其中的自由变量指向某个环境(inner 函数中的 shadowed-var 指向 outer 环境),那么即使该函数已经离开了这个环境(即调用 fun(20) 时已经离开了 outer 环境),那么该函数中的自由变量依旧要指向创建时指向的环境(即调用 fun(20) 时,函数 inner 中的 shadowed_var 依旧指向 outer 环境中的shadowed_var 而不是全局环境中的shadowed_var)。

再举几个闭包的例子:

def add(i):
    def adder()
        return i + 1
    return adder

a1 = add(1)
a2 = add(2)

注意,闭包无法修改外部函数的局部变量 以下例子是错误

def adder2(i):
    def increase():
        i += 1
        return i
    return increase

解决办法,使用 nonlocal 关键字

def adder2(i):
    def increase():
        nonlocal i
        i += 1
        return i
    return increase

最后再举一个例子

def startAt(x):
    def incrementBy(y):
        return x + y
    return incrementBy

a = startAt(5)
print('function: ', a)
print('result: ', a(5))

返回结果:

装饰器

装饰器就是在 python 中对闭包的一个具体应用。

何为装饰器

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

举个例子:

def a(func):
    # 这是一个装饰器
    def decorated()
        # do something before func
        return f()
    return decorated


@a
def b():
    # 这是一个被装饰器修饰的函数
    # do something

在这个例子中,通过闭包与@语法糖,使得函数 b 在执行前,先执行了 decorated 中的 do something before func 操作。实际执行情况如图所示: 函数 b 被 @a 修饰,@a 是一个语法糖,等同于 b = a(b),即将被修饰的函数 b 做为参数传入 a 对应的同名函数中。 特别地,多个嵌套的装饰器:

@a
@b 
@c 
def foo():
    pass

等价于 foo = a(b(c(foo)))

装饰器的作用

在 java,特别是 springboot 中,我们可以通过 AOP,在不修改一个方法的情况下,来增加这个方法的功能,比如在方法前、方法后、方法前后加入代码块,来实现类似日志记录,性能统计,安全控制等功能。同时使得这些代码与方法中的业务逻辑剥离开来。 那么在 python 中,我们就可以使用装饰器来实现通用的功能。

装饰器示例

简单装饰器

def using_logging(func):
    def wrapper():
        print('%s is running.' % func.__name__)
        return func()  # func 在这里执行,不加 return 关键字也可以
    return wrapper


@using_logging
def foo():
    print('here is foo()')


foo()

运行结果: 通过@语法糖,对 foo 函数进行重新组织,foo = using_logging(foo),同时在 using_logging 函数中,利用闭包,将被装饰器修饰的函数 foo 传递给了真正实现装饰器具体内容的函数 wrapper,并利用闭包的特性,在 wrapper 中可以调用 foo。

装饰器修饰带参数的函数

如果被修饰的函数有参数,则要利用 *args, **kwargs 传递参数。 示例:

def using_logging(func):
    def wrapper(*args, **kwargs):
        print('%s is running.' % func.__name__)
        func(*args, **kwargs)  # func 在这里执行
    return wrapper


@using_logging
def foo(name, age=None, height=None):
    print('my name is %s, age %d, height %d' % (name, age, height))


foo('charlie', 30, 177)

返回结果:

带参数的装饰器

装饰器自身可以带参数,使得装饰器的功能更加强大。 带参数的装饰器使用三级嵌套的闭包实现。

def using_logging(level):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if level == "warn":
                print('WARIN: %s is running. doc: %s' % (func.__name__, func.__doc__))
            elif level == "info":
                print('WARIN: %s is running. doc: %s' % (func.__name__, func.__doc__))
            return func(*args, **kwargs)  # func 在这里执行
        return wrapper
    return decorator


@using_logging(level="warn")
def foo(name, age=None, height=None):
    """foo doc"""
    print('my name is %s, age %d, height %d' % (name, age, height))


foo('charlie', 30, 177)

前后环绕的装饰器

在函数运行前做一下事情,再执行被装饰的函数,最后再执行一些事情。

def count_time(func):
    def wrapper(*args, **kwargs):
        st = time.time()
        func(*args, **kwargs)  # 这里不加 return 关键字
        cost_time = time.time() - st
        print("%s 共耗时 %f" % (func.__name__, cost_time))
    return wrapper


@count_time
def foo(name, age=None, height=None):
    """foo doc"""
    print('my name is %s, age %d, height %d' % (name, age, height))


foo('charlie', 30, 177)