python生产实战 python 闭包之庖丁解牛篇

读完需要 16分钟

速读仅需 6 分钟

/ python 生产实战 从闭包到中间件 /

注:这篇文章很长,但我保证你看完后能获得足够多对闭包的了解,也会彻底理解清楚中间件的实现原理。

闭包这个概念无论在你面试 python 开发工程师 的时候还是在日常的 python 开发过程中都有一些涉及,笔者之前在研究 Tornado 源码的过程中看到大量的使用闭包去实现特定功能的案例,上一篇中分享了如何通过中间件的方式解决生产环境中的实际问题从而拿到公司今年涨薪名额的案例。本篇我们就从一个闭包的概念出发来一步步分析并完成一个可用的中间件功能。

1 什么是闭包

闭包是一个广泛存在的概念,在数学,拓扑学以及计算机科学中都有这个它的身影,虽然都叫这个名字,但是在定义上还是有所区别,此闭包非彼闭包。

1.1 计算机中的闭包

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

1.2 数学领域中的闭包

集合 S 是闭集当且仅当 Cl(S)=S(这里的 cl 即 closure,闭包)。特别的,空集的闭包是空集,X 的闭包是 X。集合的交集的闭包总是集合的闭包的交集的子集(不一定是真子集)。有限多个集合的并集的闭包和这些集合的闭包的并集相等;零个集合的并集为空集,所以这个命题包含了前面的空集的闭包的特殊情况。无限多个集合的并集的闭包不一定等于这些集合的闭包的并集,但前者一定是后者的父集。

关于各个领域中闭包的解释,我们就说这些。我们的目的是让大家清楚这个词的使用范围是很广的,只需了解即可不是我们本节的重点。

2 闭包使用场景

从本节往后的全文,若无特殊说明,提到闭包都指的是计算机领域的闭包。闭包的使用场景是很丰富的,我简单的举几个例子:

  1. 因为闭包只有在被调用时才执行操作,所以它可以被用来定义控制结构。例如:在 Smalltalk 语言中,所有的控制结构,包括分歧条件(if/then/else)和循环(while 和 for),都是通过闭包实现的。用户也可以使用闭包定义自己的控制结构。
  2. 多个函数可以使用一个相同的环境,这使得它们可以通过改变那个环境相互交流。
  3. 闭包可以用来实现对象系统。

以上所讲的都是脱离语言讲的使用场景,当然结合各个语言其使用场景可以演化出多种多样,这些都不是本文讨论的重点,在此忽略不提。

3 C/C++ 中的闭包思想

3.1 C 中类似闭包的结构

在 C 语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的 void*指针用以保存用户数据。这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。

3.2 C++ 中类似闭包的结构

C++允许通过重载operator()来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。它们可以在运行时创建,保存状态,但是不能如闭包一般隐式获取局部变量。C++标准委员会正在考虑两种在 C++中引入闭包的建议(它们都称为 lambda 函数)。这些建议间主要的区别在于一种默认在闭包中储存全部局部变量的拷贝,而另一种只存储这些变量的引用。这两种建议都提供了可以覆盖默认行为的选项。若这两种建议之一被接受,则可以写如下代码:

void foo(string myname) {
        typedef vector<string> names;
        int y;
        names n;
        // ...
        names::iterator i =
         find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;});
        // 'i' is now either 'n.end()' or points to the first string in 'n'
        // 'i' 现在是'n.end()'或指向'n'中第一个
        // 不等于'myname'且长度大于'y'的字符串
}

以上简单的介绍了在 C/C++ 中存在的类似闭包的思想供大家参考。

4 python 预备知识之变量与作用域

本节开始以 python 语言为例来一步步拆解闭包,本节核心是讲讲变量相关的预备知识,若你对 python 的变量作用域已是很清楚了则可跳过直接看下一小结。

我定义一个函数 get_name,其 name 作为函数 get_name 的局部变量,然后我们在这个函数外部来获取这个 name 的值,请结合代码思考一下最终的执行后的结果是什么?

def get_name():
    name = "haishiniu"

print(name)

我们先给出结论: 执行之后的结果是 NameError: name 'name' is not defined。如何,你答对了吗?为何会输出这样的结果呢?

为了解释这个点我们先看一下在 python 中,一个变量被解释器解释的时候,其规则是怎么样的。当一个变数被使用时,会遵循 LEGB 的规则,也就是 Local、Enclosing、Global 与 Builtins。

1.Local 很好理解,即作用于同一作用域的局部名称

2.Enclosing 即 Enclosing Scope,闭包中的核心,后续会详细解释

3.Global 全局名称

4.Builtins 内建,比如一些内建的函数: str()、int()...

那什么是 Enclosing Scope ?想要有 Enclosing Scope 首先都有 Scope 的存在,而函数就是创建 Scope 的方式。

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

def get_scope():
    name = "haishiniu"
    def get_name():
        print(name)

# Output: "haishiniu"
get_scope()
# NameError: name 'name' is not defined
print(name)

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

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

5 python 闭包

对上节中的代码进行修改符合 python 对闭包的定义,可得到如下代码。

def get_scope():
    name = "haishiniu"
    def get_name():
        print(name)
    return get_name

test = get_scope()
test()    # Output: "haishiniu"

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

由于 get_scope() 返回了 get_name,且 get_name 中使用了处于 get_scope Scope 中的变量 name,于是 get_name 将 name 捕获,形成了闭包,此时 name 便是一个自由变量。

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

一句话总结: 闭包是持有外部环境变量的函数。

5.1 闭包无法修改自由变量

本小结是 python 开发面试中的常客,请务必理解掌握。

这里的无法修改是指不能改变自由变量的地址。我们先看一段代码:

def get_num_scope():
    num = 1
    def get_num ():
        num = 2
        print(num)
    print(num)
    get_num()
    print(num)
    return get_num

test = get_num_scope()
test()

我们看一下输出:

1 2 1 2

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

对于字典以及数组这类可变类型,是可以对自由变量值做出改变的。我们再来看一段代码:

def get_list_data_scope():
    list_data = [1]
    def get_list_data():
        list_data.append(2)
        print(list_data)
    print(list_data)
    get_list_data()
    print(list_data)
    return get_list_data

test = get_list_data_scope()
test()

我们在看一下输出结果:[1 ][1, 2][1, 2][1, 2, 2]

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

5.2 循环与闭包配合

本小结是 python 开发面试中的常客,请务必理解掌握。不知道你出去面试的时候有没有碰到过类似的一个题目:

func_list = []
for i in range(3):
    def multi_f():
        return i * 2
    func_list.append(multi_f)

for f in func_list:
    print(f())

请问上述片段输出的结果是什么呢?记得三年前出去面试的时候 我就傻傻分不清的说输出的结果是:0,2,4。然后回去后我重新写了这段代码执行之后输出的结果是:4,4,4。

这是为什么呢?在之前解释闭包这个概念的时候有提到过,闭包中的自由变量来源必须是 Enclosing Scope 中的变量,而 Python 的中的循环并没有 Scope 这个概念,我们通过一个代码片段看一下:

for i in range(100):
    out_put = i + 1

print(out_put)

输出的结果为:100。

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

func_list = []

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

for f in func_list:
    print(f())

输出结果为:0 2 4

6 从闭包实现中间件功能

在上篇处理中间件的问题时候查看了 python 主流框架的 中间件的实现源码,本次想结合闭包来实现一个类中间件的功能,主要是分装一个类 server 服务端对外提供服务,主要实现:

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

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

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

我们看一下实现代码:

# 在会话中保存上下文
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)                

                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.add_middleware
def the_first_middleware(ctx, *args, **kwargs):
    print("The first middleware ")
    return ctx.next(*args, **kwargs)


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


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


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

server = Server()

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

输出结果:

原创不易,只愿能帮助那些需要这些内容的同行或刚入行的小伙伴,你的每次 点赞、分享 都是我继续创作下去的动力,我希望能在推广 python 技术的道路上尽我一份力量,感谢大家。

建议大家可以使用 腾讯云服务器 进行云上测试和验证自己的代码(CDN)

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
python生产实战 python 闭包之庖丁解牛篇
注:这篇文章很长,但我保证你看完后能获得足够多对闭包的了解,也会彻底理解清楚中间件的实现原理。
<<上一篇
下一篇>>