浅谈 Python 中的闭包与中间件封装

2019-10-22

这两天看了一些中间件框架相关的代码,发现闭包的应用很多,由于之前对闭包这个概念似懂非懂,所以我借此机会学习了一番,然后把成果在此总结记录一下。

什么是闭包

闭包这个概念存在甚广,数学,拓扑学以及计算机科学中都有这个它的身影,虽然名称相同,但是在定义上还是有所区别。

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

光听概念会有些抽象,我们来用 Python 一步一步举例说明何谓闭包。

变量作用域

且看下面这个代码,x 作为函数 a 的局部变量,在函数外部是无法访问的。x 的变量作用域现在仅限函数内部。

def a():
    x = 1

# NameError: name 'x' is not defined
print(x)

当一个变数被使用时,会遵循 LEGB 的规则,也就是 Local、Enclosing、Global 与 Builtins。

  • Local 很好理解,即作用于同一作用域的局部名称
  • Enclosing 即 Enclosing Scope,闭包中的主角,我们后文会解释
  • Global 全局名称
  • Builtins 内建,比如一些内建的函数 str()

那么什么是 Enclosing Scope?想要有 Enclosing Scope 首先都有 Scope 的存在,而函数就是创建 Scope 的方式。上方会报错的代码中,函数 a 的创建就产生了一个 Scope,而 x 就在这个 Scope 中。那么根据 LEGB 查询原则,我们可以构造以下的代码,来创建一种 Local 中没有查询到,需要到 Enclosing 中查询的情况。

def a():
    x = 1
    def b():
        print(x)

# Output: 1
a()
# NameError: name 'x' is not defined
print(x)

当我们在 b 函数内部使用 x 的时候,遵循 LEGB 原则,由于 Local 中没有找到名为 x 的变量,于是到 Enclosing 中寻找,即函数 a 所创建的 Scope 中去寻找,然后使用这个处于 b 函数外层的变量。然而如同上面的例子一样,随着 a 函数的运行结束,x 也随之消亡了,我们在外层使用 x 同样是行不通的。

那么有没有什么方法可以让我们脱离 a 函数本身的作用范围,即能不能在 a 函数结束运行之后让局部变量 x 还可以被访问得到呢?答案就是闭包。

闭包

我们修改上面的代码,得到如下的结果。

def a():
    x = 1
    def b():
        print(x)
    return b

test = a()
test()    # Output: 1

一般情况下,函数中的局部变量仅在函数的执行期间可用,一旦 a() 执行过后,我们会认为 x 变量将不再可用。然而真实情况是我们成功输出了 x 的值,即便此时 a 函数早已经执行结束————这种情况下便形成了一个闭包。

由于 a() 返回了 b,且 b 中使用了处于 a Scope 中的变量 x,于是 bx 捕获,形成了闭包,此时 x 便是一个自由变量。再来回看闭包的定义:

闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

此时闭包的概念便明晰了,再用大白话讲一下便是:闭包是持有外部环境变量的函数。

闭包的一些细节

我们再来看几个例子,进一步展示 Python 中闭包的细节。

闭包无法“修改”自由变量

注意这个修改是打引号的,更准确的说法应该是闭包不能改变自由变量的地址。

def a():
    x = 1
    def b():
        x = 2
        print(x)
    print(x)
    b()
    print(x)
    return b

test = a()
test()

输出如下:

# Output
1    # From the first print(x) inside a()
2    # From the print(x) inside b() which is called by a()
1    # From the second print(x) inside a()
2    # From the print(x) inside b() which is called by test()

可以看到 x = 2 只能在 b() 内部生效,而作为闭包一部分的自由变量 x 的值无论如何始终为 1,无法改变。然而自由变量的值真的无法改变吗?事实上,由于 int 类型在 Python 中为不可变类型,在 x = 2 这个表达中,解释器实质上只是把符号 x 重新分配给了内存中值为 2 的一个 PyObject,参与闭包形成的自由变量的地址依然为内存中值 1 的地址,所以在这个现象中无法改变闭包的值实质上源自 Python 本身的特性,而非闭包之机制。对于字典以及数组这类可变类型,是可以对自由变量值做出改变的。

def a():
    x = [1]
    def b():
        x.append(2)
        print(x)
    print(x)
    b()
    print(x)
    return b

test = a()
test()

输出如下:

# Output
[1]
[1, 2]
[1, 2]
[1, 2, 2]

由此可以看出 Python 在内部实现闭包时,与嵌套函数所绑定的其实是自由变量的地址,我们是可以成功改变地址指向之内容的,而无法改变形成闭包变量地址之本身。

闭包与循环

再来看看闭包和循环之间搭配的一个例子。

func_list = []

for i in range(3):
    def a():
        return i * 2
    func_list.append(a)

for f in func_list:
    print(f())

在了解闭包概念后,直觉告诉我们这个例子的输出应该是 0, 2, 4,然而运行的实际结果却是 4, 4, 4。这是为什么?实际上,在之前解释闭包这个概念时我们说过,闭包中的自由变量来源必须是 Enclosing Scope 中的变量,而 Python 的中的循环并没有 Scope 这个概念:

>>> for i in range(10):
...     temp = i + 1
...
>>> temp
10

temp 是在循环中定义的变量,但实际上 Python 中的循环并不构成一个 Scope,所以实际上循环结束后我们依然可以访问 temp,自然而然这个值就是最后一次循环得到的结果。此时也就不难解释之前的代码为何输出了 4, 4, 4,由于 i 并不满足成为自由变量的资格(不存在 Scope),故在调用 f() 时我们拿到的 i 值始终为 2。要实现循环中的闭包,我们只需要再加一个函数,形成一个 Scope 就可以了。

func_list = []


for i in range(3):
    def a(x):
        def b():
            return x * 2
        return b
    func_list.append(a(i))

for f in func_list:
    print(f())

此时的输出就变为了 0, 2, 4

闭包的实战

为了展示闭包在实践中的运用,我封装了一个类似 Web Server 的 Server 模型,它主要有以下两个主要功能:

  • 使用装饰器 @server.add_middleware 添加自定义中间件
  • 使用装饰器 @server.add_func('core_func_name') 添加自定义核心件

在使用 Server.initilize() 进行封装初始化后,可以直接通过 Server.core_func_name() 来运行已经被所有自定义中间件包裹的自定义核心件。

在具体实现中,_load_middleware 这个方法通过循环和闭包把中间件一层一层包裹到核心件上去,最后返回最外层的入口。

代码的 Gist

# 上下文类,本例中主要用于存储当前调用的下文,即内层中间件
class Context():
    def __init__(self):
        self._next = []

    @property
    def next(self):
        return self._next


class Server():
    def __init__(self):
        self._middlewares = []   # 所有添加中间件
        self._funcs = {}    # 所有添加的核心件

    def add_middleware(self, middleware_func):
        self._middlewares.append(middleware_func)
        return middleware_func

    def add_func(self, name):
        def decorate(func):
            self._funcs.setdefault(name, func)
            return func
        return decorate

    def _load_middleware(self, ctx, func):
        def next(*args, **kwargs):
            return func(ctx, *args, **kwargs)
        for middleware in reversed(self._middlewares):
            # 使用闭包来封装中间件
            def f(middleware=middleware, next=next):
                def new_next(*args, **kwargs):
                    ctx._next = next
                    return middleware(ctx, *args, **kwargs)
                new_next.__name__ = getattr(middleware, '__name__')
                return new_next
            next = f()
        return next

    def _wrap(self, func):
        def f(*args, **kwargs):
            ctx = Context()
            return self._load_middleware(ctx, func)(*args, **kwargs)
        return f

    def initilize(self):
        for name, func in self._funcs.items():
            self.__setattr__(name, self._wrap(func))


server = Server()


@server.add_middleware
def the_first_middleware(ctx, *args, **kwargs):
    print("The first one")
    return ctx.next(*args, **kwargs)


@server.add_middleware
def the_second_middleware(ctx, *args, **kwargs):
    print("The second one")
    return ctx.next(*args, **kwargs)


@server.add_middleware
def the_last_middleware(ctx, *args, **kwargs):
    print("The last one")
    return ctx.next(*args, **kwargs)


@server.add_func('core_func')
def core_func(ctx, *args, **kwargs):
    return "The Core Function"


server.initilize()
print(server.core_func())

最后的输出如下,我们可以看到在调用核心函数的同时,中间件已经被自动的执行了。

The first one
The second one
The last one
The Core Function
Tagged in : Python Closure Middleware