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

  • python生产实战 从闭包到中间件
    • 什么是闭包
      • 计算机中的闭包
      • 数学领域中的闭包
    • 闭包使用场景
    • C/C++ 中的闭包思想
      • C 中类似闭包的结构
      • C++ 中类似闭包的结构
    • python预备知识之变量与作用域
    • python 闭包
      • 闭包无法修改自由变量
      • 循环与闭包配合
    • 从闭包实现中间件功能

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

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

什么是闭包

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

计算机中的闭包

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

数学领域中的闭包

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

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

闭包使用场景

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

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

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

C/C++ 中的闭包思想

C 中类似闭包的结构

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

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++ 中存在的类似闭包的思想供大家参考。

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 还可以被访问得到呢?答案就是闭包

python 闭包

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

def get_scope():name = "haishiniu"def get_name():print(name)return get_nametest = get_scope()
test()    # Output: "haishiniu"

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

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

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

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

闭包无法修改自由变量

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

这里的无法修改是指不能改变自由变量的地址。

我们先看一段代码:

def get_num_scope():num = 1def get_num ():num = 2print(num)print(num)get_num()print(num)return get_numtest = 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_datatest = get_list_data_scope()
test()

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

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

循环与闭包配合

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


func_list = []for i in range(3):def multi_f():return i * 2func_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 + 1print(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 * 2return get_datafunc_list.append(get_data_scope(i))for f in func_list:print(f())

输出结果为:
0
2
4

从闭包实现中间件功能

在上篇处理中间件的问题时候查看了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 = []@propertydef next(self):return self._nextclass Server():def __init__(self):self._middlewares = []   # 中间件队列self._funcs = {}    # 自定义核心件映射容器def add_middleware(self, middleware_func):"""添加中间件"""self._middlewares.append(middleware_func)return middleware_funcdef add_func(self, name):"""注册自定义核心件"""def decorate(func):self._funcs.setdefault(name, func)return funcreturn decoratedef _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 = nextreturn middleware(ctx, *args, **kwargs)return new_nextnext = f()return nextdef _wrap(self, func):def f(*args, **kwargs):ctx = Context()return self._load_middleware(ctx, func)(*args, **kwargs)return fdef 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 技术的道路上尽我一份力量,感谢大家。

python生产实战 python 闭包之庖丁解牛篇相关推荐

  1. 『Python开发实战菜鸟教程』工具篇:手把手教学使用VSCode开发Python

    文章目录: 0X01:引子 Python学习资料及开发工具介绍 0x02:实操 基于 Conda 的 Python 安装 VSCode 的安装使用 安装 插件管理按钮使用介绍 VSCode 如何管理项 ...

  2. python http2_实战 | Python使用HTTP2实现苹果原生推送

    原标题:实战 | Python使用HTTP2实现苹果原生推送 本文约 4405 字,读完可能需要 6 分钟. 作者:风中纸鸢 原文: 说起苹果的推送,可能很多开发人员就开始头疼了,因为实现苹果推送服务 ...

  3. python语言实战-Python实战-从菜鸟到大牛的进阶之路 pdf完整版

    Python是一种解释型.面向对象.动态数据类型的高级程序设计语言,现在它已经成为最受欢迎的程序设计语言之一.本专题收录了Python编程实战教程,分享给大家. 适用人群:Python 进阶学习者.W ...

  4. python数据分析实战-Python数据分析案例实战(慕课版)

    基本信息 书名:Python数据分析案例实战(慕课版) :59.80元 作者:王浩,袁琴,张明慧 著 出版社:人民邮电出版社 出版日期:2020_06_01 ISBN:9787115520845 字数 ...

  5. python爬虫实战——js逆向登录第一篇:鹏华基金

    鹏华基金 鹏华基金 - 登录 https://aj.phfund.com.cn/login.html 小弟不爱写文字(CSDN的编辑器我不会用.),因此大多数以图片为准~各位大佬看官请见谅~ 如果各位 ...

  6. python爬虫实战-python爬虫实战一:分析豆瓣中最新电影的影评

    简介 刚接触python不久,做一个小项目来练练手.前几天看了<战狼2>,发现它在最新上映的电影里面是排行第一的,如下图所示.准备把豆瓣上对它的影评(短评)做一个分析. 目标总览 主要做了 ...

  7. python爬虫模块取cookie_[ Python爬虫实战 ] Python使用cookies - pytorch中文网

    一.Cookie登录简介 Cookie是一种服务器发送给浏览器的一组数据,用于浏览器跟踪用户,并访问服务器时保持登录状态等功能.基本所有的网站,我们都可以通过操作Cookie来进行模拟登录,所以我们只 ...

  8. Python应用实战-Python五个实用的图像处理场景

    1.生成手绘图片 现在很多软件可以将照片转换成手绘形式,python也可以实现,而且定制化更强,可批量转换. 这里用到pillow库,这是非常牛逼且专业的Python图像处理库 原图: 生成手绘后: ...

  9. python怎么写中文至excel_[ Python爬虫实战 ] python 操作excel以及解决中文报错 - pytorch中文网...

    有时候我们可以把一些简单的数据存储进Excel文件中,比如我们的我们爬取了某网站的用户ID,昵称,我们可以存储进Excel中. 环境安装 xlwt,xlrd是python读写操作excel的模块,你可 ...

最新文章

  1. 双圆弧插值算法(三,代码实现)
  2. Hadoop MapReduce编程 API入门系列之最短路径(十五)
  3. Android污点分析工具flowdroid源码简析
  4. 加州大学研发全柔性汗液传感器,实时监控人体健康
  5. 苏州软件测试11k工资要什么水平,3个月从机械转行软件测试,他的入职薪资是11K...
  6. IDR、CRA、BLA、RASL、RADL、closed-gop、open-gop
  7. android音视频指南-支持的媒体格式
  8. pyinstaller利用配置文件打包
  9. 多语言跨平台远程过程调用【Avro】
  10. 什么是机器学习?(上)
  11. 为什么我选择用 C 编写游戏?
  12. ora-12560:协议适配器错误
  13. HDU1013 Digital Roots
  14. c语言自学教程——字符函数和字符串函数
  15. java调用js tel,jQuery国际电话区号选择插件intlTelInput.js
  16. java给服务器创建文件夹,java服务器创建文件夹
  17. elasticsearch 在不是 not_analyzed 的前提下如何全匹配的效果
  18. linux 显示器识别工具,Linux 显示器未正确识别 最佳分辨率 Ubuntu 10.10 X1
  19. 空空导弹发展及作战特点
  20. 如何用java代码给Word文档添加水印?

热门文章

  1. Httpd服务重定向配置
  2. 数据分析---arXiv论文数据统计
  3. 【优秀的无损音乐播放器】Audirvana for Mac 3.5.9
  4. pygraphviz win7安装报错解决
  5. flash游戏代码html5,Flash贪吃蛇游戏AS代码翻译
  6. 天涯明月刀无法显示服务器,天涯明月刀登录不进去怎么办 pc端手机端服务器已满怎么解决_游戏369...
  7. LMG3410R050功率放大级解决方案
  8. python音频处理库librosa基本操作
  9. 问题小结:解决Ubuntu18.04系统无法连接WIFI
  10. php禁用session和cookies,session与cookie的区别是什么?如果客户端禁用了cookie功能,将会对session有什么影响?...