[Python] Python-从装饰器(decorator)谈到闭包(closure)

为什么要在函数中返回一个函数?

由Jeza Chen 发表于 January 11, 2020

该文章只是一篇对闭包和装饰器的简单介绍。

一开始看裘宗燕老师所写的《程序员学Python》这本书时,对闭包十分不理解,琢磨着为什么要在一个函数中返回一个函数呢?只能做个标记继续往后看,直到看到装饰器这一章时,细细思考其中原理方才发现闭包的重要性。

1. 什么是装饰器

装饰器(decorator),是Python语言的一个语法糖(Syntactic sugar)。 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

比如有这么一个装饰器decorator:

@decorator
def func(param):
    ...

那么该代码等价于:

func = decorator(func)

即,装饰器接受一个函数作为参数,在内部对函数增添一些功能后,再返回装饰后的函数对象,并赋值给原来的变量。

2. 由装饰器到闭包

一个函数,且里面再包含一个函数(内部函数),那么该内部函数就是一个闭包(closure)。

我们先定义一个装饰器:

import time
from functools import wraps


def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result

    return wrapper


@timethis
def func(n):
    while n >= 0:
        n -= 1

在解释器中查看结果:

>>> func(1999999)
func 0.10681343078613281

在这里,函数timethis()所包含的内部函数wrapper()就是一个闭包,而外部函数timethis()则是一个装饰器。经过该装饰器包装过的函数,支持运行时间记录的功能。其原理也很简单:将原函数func作为一个参数传进函数timethis()中,其内部函数wrapper()加上特定的功能后,再调用原来的函数func。最后,外部函数timethis()在返回闭包wrapper()给外部的func变量。从此,func变量不再是原来的函数对象,而是经过装饰后的、新的函数对象。在这里,我们就可以窥见闭包的重要作用。

除此之外,闭包的作用域也是一个重要的话题。闭包能够访问函数体之外定义的非全局变量(简单来说,就是外部函数定义的变量它也是可以访问的),而这个非全局变量指的就是自由变量。可以这么说,闭包就是一个拥有状态的函数

修改上面的例子以说明:

import time
from functools import wraps


def timethis(func):
    time_history = []

    @wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal time_history
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        exec_time = end - start
        print(func.__name__, exec_time)
        time_history.append(exec_time)
        print("history: ", time_history)

        return result

    return wrapper


@timethis
def func(n):
    while n >= 0:
        n -= 1

在这里,装饰器增加了一个功能,能够保存历史的运行时间(需要使用nonlocaltime_history变成自由变量)。我们来测试一下是否可行:

>>> func(10000)
func 0.0005609989166259766
history:  [0.0005609989166259766]
>>> func(2333)
func 0.0001125335693359375
history:  [0.0005609989166259766, 0.0001125335693359375]
>>> func(233333)
func 0.01238703727722168
history:  [0.0005609989166259766, 0.0001125335693359375, 0.01238703727722168]

从中可以看到,闭包是可以保存状态的!这个和普通的函数还是有点差异的,跟静态函数(static method)有那么一点点类似。

3. 总结

总的来说,闭包就是藏在一个函数里的函数,由于这一点,它相比普通函数拥有保存状态的功能。因此,它也是装饰器的重要基础。

当然,装饰器也是可以使用类来实现的。这里就不多说了。更多细节部分可以参考Python文档或者《Python Cookbook(第三版)》。