彻底理解Python中的闭包和装饰器(上)
什么是闭包
闭包(Closure)其实并不是Python独有的特性,很多语言都有对闭包的支持。(当然,因为Python是笔者除C/C++之外学习的第二门语言,所以也是第一次遇到闭包。)简而言之,闭包实际上就是——函数中定义的函数。
这种程序结构的主要作用是:使得函数中的局部变量可以常驻内存,即使在函数返回之后(函数生命期结束后)。在这个意义上它的作用与C++中的static静态变量类似,当然不完全相同。
Python中闭包的定义和使用
在Python中,一个典型的闭包可以这样定义:
def outer(arg):
temp = 10
def inner():
_sum = temp + arg # 内函数引用了外函数的局部变量
print('_sum =', _sum)
return _sum
return inner # 外函数返回了内函数的引用
在这里有两个嵌套的函数,不妨叫他们外函数和内函数。可以看到闭包有两个显著的特点:
- 内函数引用了外函数的局部变量。
- 外函数返回了内函数的引用(函数名)。
符合以上两点,Python解释器会认为这是一个闭包。这时如果外函数的生命期结束了,在外函数中创建的局部变量并不会像通常一样被销毁,而是会留在内存中。这样当下次调用内函数时,就能够继续使用这些局部变量。
通过下面的分析可以看到,调用内函数正是通过外函数返回的函数指针(Python中没有指针变量,出于C++习惯笔者认为把它称作指针比较易于理解,没有学过C/C++的读者理解成返回了内函数的地址即可)。
闭包代码分析
我们来仔细分析上面的代码。
如果读者有C/C++经验,那么理解起来将会轻松许多。C++严格的语法要求函数必须先定义再调用,在Python并没有不同。因此需要牢记一点:在代码段中,函数的定义是不会被执行的,在理解代码时def下的所有内容都先跳过,到调用函数时再回来看它。
按照这种阅读顺序,在外函数outer()中实际上只做了三件事情:
- 定义局部变量temp
- 定义内函数inner()
- 返回内函数inner,实际上是返回了内函数的指针。
调用这个闭包时,首先用一个变量保存函数对象(的指针):
f = outer(2)
执行这句话时,就完成了上面所说的1~3条,f实际上是outer()返回的inner()的指针。注意,第2条只做了函数的定义,第3条只返回了函数的引用。完成这两件事的时候,实际上都还没有执行内函数inner()。所以执行这句代码后的输出为:
>
对,啥都没有。因为任何shell中进行输出的语句还没有被执行。这是透彻理解闭包非常重要的一点。忽略这一点很容易造成所谓的“闭包陷阱”。
那么如何调用内函数呢?就要用刚刚用来保存函数指针的变量f:
x = f()
print('x = ', x)
上面的两句代码,实际上通过函数指针f执行了内函数inner()。执行上面的所有代码,输出为:
_sum = 12
x = 12
再次强调:
直到使用函数指针调用内函数,内函数才会被执行。
需要说明,虽然在闭包中定义的局部变量常驻内存中,但在闭包外这些变量仍然是不可访问的。如上面的temp变量,只有通过函数指针f才可以访问,在函数外引用该变量会报错变量不存在。这与C++中的静态变量相同,即生命期比局部变量长,但可见性与局部变量相同。
修改闭包的局部变量
外函数中的局部变量虽然在内函数中可以引用(使用),但不能够重新赋值。
执行如下闭包函数:
def outer(arg):
temp = 10
def inner():
_sum = temp + arg
temp += 1 #在内函数中尝试改变temp的值
print('_sum = ', _sum)
return _sum
return inner
会报如下错误:
UnboundLocalError: local variable 'temp' referenced before assignment
这意味着对于内函数来说,外函数中的局部变量只是一个可以使用的常量,它不能被修改。如果在内函数中重新定义一个同名变量,那么它会屏蔽掉外函数中的变量,即优先使用“更局部”的变量。
这实际上是由Python本身的语法特性造成的。在Python中,一个函数可以任意读取全局数据,但要修改时必须符合如下条件之一:
- 全局变量使用global声明
- 全局变量是可变类型数据
在闭包中这一点是类似的。如果想要修改外函数中的变量,可以使用以下两种方法之一:
- 使用nonlocal声明变量
def outer(arg):
temp = 10
def inner():
nonlocal temp #用nonlocal声明变量,表示要到上一层变量空间寻找该变量
_sum = temp + arg
temp += 1 #此处修改temp的值,不会报错
print('_sum = ', _sum)
return _sum
return inner
f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)
代码执行输出为:
_sum = 12
x = 12
_sum = 13
x = 13
- 将变量改为可变类型数据,如list
def outer(arg):
temp = [10]
def inner():
# nonlocal temp
_sum = temp[0] + arg
temp[0] += 1
print('_sum = ', _sum)
return _sum
return inner
f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)
输出结果相同。
从以上代码也可以看出,闭包中常驻内存的局部变量只有一份。当重复调用内函数时,访问的是同一处变量。
闭包的参数
闭包的外函数和内函数都是函数,因此都可以接受参数,区别只在于参数是创建函数指针时传入,还是实际调用内函数时传入。
如果在创建函数指针时传入,那么该参数在之后的调用中都会保持原值。以本文最开始的闭包代码为例,传给外函数的参数arg,与在外函数中定义的局部变量temp地位是完全相同的。
相应地,传给内函数的参数则可以在每次调用的时候都不一样。执行如下代码:
def outer():
temp = 10
def inner(arg):
_sum = temp + arg
print('_sum = ', _sum)
return _sum
return inner
f = outer()
x = f(2)
print('x = ', x)
x = f(5)
print('x = ', x)
输出为:
_sum = 12
x = 12
_sum = 15
x = 15
闭包陷阱
引用廖雪峰教程中的例子:
def count():
fs = []
for i in range(1, 4):
def func():
return i*i
fs.append(func)
return fs
f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())
上面的闭包创建了一个函数的list,并将这个list返回。这样会造成闭包陷阱,编写者也许原来希望返回的是1、2、3的平方值,但实际上执行的结果是:
9
9
9
原因就是之前强调的,内函数的指针被创建时,它实际上还没有被执行。
在上面内函数的循环中,每次循环只做了一件事,创建一个函数func()的指针并放入list。当真正调用三个内函数时,局部变量i已经变成3了,因此三个函数的返回值都是3。
使用闭包时必须牢记:
不要返回任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?这时候只能再嵌套一个函数并立即执行它,将函数参数绑定到循环变量的当前值。代码如下:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
上面的代码实际上是两层嵌套的闭包。每次循环里,都使用当前的循环变量i立即调用了函数f(i),它的意义是创建了函数指针并放入list。具体来说,是调用内层闭包的外函数,返回内层闭包的内函数指针。
当各个函数指针被创建时,已经将当前循环变量传入闭包。对于后续的操作来说,每一个内层闭包拥有独立且不变的局部变量。当外层闭包返回函数list时,也就避免了闭包陷阱。
小结
- 闭包的两个特征:内函数引用外函数的局部变量,外函数返回内函数的指针。
- 外函数指针被创建时,内函数未被执行,直到使用函数指针调用内函数才会被执行。
- 使用闭包时,不要返回任何循环变量或后续会发生变化的变量。
彻底理解Python中的闭包和装饰器(上)的更多相关文章
- 轻松理解python中的闭包和装饰器 (下)
在 上篇 我们讲了python将函数做为返回值和闭包的概念,下面我们继续讲解函数做参数和装饰器,这个功能相当方便实用,可以极大地简化代码,就让我们go on吧! 能接受函数做参数的函数我们称之为高阶函 ...
- 轻松理解python中的闭包和装饰器(上)
继面向对象编程之后函数式编程逐渐火起来了,在python中也同样支持函数式编程,我们平时使用的map, reduce, filter等都是函数式编程的例子.在函数式编程中,函数也作为一个变量存在,对应 ...
- python中的闭包和装饰器
重新学习完了函数,是时候将其中的一些重点重新捋一捋了,本次总结的东西只有闭包和装饰器 1.闭包 闭包是python函数中的一个比较重要功能,一般闭包都是用在装饰器上,一般学完闭包就会去学习装饰器,这俩 ...
- 21.python中的闭包和装饰器
python中的闭包从表现形式上定义(解释)为:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure). 以下说明主要针对 python ...
- Python 中的闭包与装饰器
闭包(closure)是函数式编程的重要的语法结构.闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性. 如果在一个内嵌函数里,对在外部函数内(但不是在全局作用域)的变量进行引用,那么内嵌函数 ...
- python中的闭包与装饰器
#原创,转载请留言联系 装饰器的本质就是闭包,所以想知道装饰器是什么,首先要理解一下什么是闭包. 闭包 1. 外部函数返回内部函数的引用.2. 内部函数使用外部函数的变量或者参数. def outer ...
- 聊聊Python中的闭包和装饰器
1. 闭包 首先我们明确一下函数的引用,如下所示: def test1(): print("--- in test1 func----") # 调用函数 test1() # 引用函 ...
- python中函数总结之装饰器闭包
1.前言 函数也是一个对象,从而可以增加属性,使用句点来表示属性. 如果内部函数的定义包含了在外部函数中定义的对象的引用(外部对象可以是在外部函数之外),那么内部函数被称之为闭包. 2.装饰器 装饰器 ...
- 理解Python中的闭包
1.定义 闭包是函数式编程的一个重要的语法结构,函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式).在面向过程编程中,我们见到过函数(function):在面向对象编程中,我们见 ...
- 第十七篇 Python函数之闭包与装饰器
一. 装饰器 装饰器:可以拆解来看,器本质就是函数,装饰就是修饰的意思,所以装饰器的功能就是为其他函数添加附加功能. 装饰器的两个原则: 1. 不修改被修饰函数的源代码 2. 不修改被修饰函数的调用方 ...
随机推荐
- 第一个Django应用 - 第五部分:测试
一.自动化测试概述 什么是自动化测试 测试是一种例行的.不可缺失的工作,用于检查你的程序是否符合预期. 测试可以划分为不同的级别.一些测试可能专注于小细节(比如某一个模型的方法是否会返回预期的值?), ...
- 用prometheus监控Nginx
GitHub上官方地址:https://github.com/knyar/nginx-lua-prometheus 告警规则地址:https://awesome-prometheus-alerts.g ...
- 实战---在Portainer中编排docker-compose.yml文件
选择要部署容器的主机上,不用事先安装配置docker-compose 官方示例文档地址,2.0版本的:https://docs.docker.com/compose/compose-file/comp ...
- NSIS 检测默认浏览器
#检测默认浏览器 #编写:水晶石 #原理:用FindExecutable函数查找htm关联程序路径与名称,然后分析字串中包含的可执行文件名. !include "LogicLib.nsh&q ...
- Leetcode链表
Leetcode链表 一.闲聊 边学边刷的--慢慢写慢慢更 二.题目 1.移除链表元素 题干: 思路: 删除链表节点,就多了一个判断等值. 由于是单向链表,所以要删除节点时要找到目标节点的上一个节点, ...
- 糟了,线上服务出现OOM了
前言 前一段时间,公司同事的一个线上服务OOM的问题,我觉得挺有意思的,在这里跟大家一起分享一下. 我当时其实也参与了一部分问题的定位. 1 案发现场 他们有个mq消费者服务,在某一天下午,出现OOM ...
- CEOI2020 道路(Roads) Solution
直接来构造. 考虑扫描线.从左到右扫,考虑当前扫到了一个左端点,我们把这个左端点连到其他点上. 我们可以找到这个点下方离他最近的线段,并且记下每条线段上方在扫描线左侧且最靠右,与这条线段中间没有其他线 ...
- 【NOI2016】 循环之美 题解
Solution 由数论基础知识 答案即为$$\sum_{i = 1}^n\sum_{j = 1}^m[i \perp j][j \perp k]$$ 莫反套路可化为$$\sum_{d = 1}\mu ...
- 从0开始写一个简单的vite hmr 插件
从0开始写一个简单的vite hmr 插件 0. 写在前面 在构建前端项目的时候,除开基本的资源格式(图片,json)以外,还常常会需要导入一些其他格式的资源,这些资源如果没有第三方vite插件的支持 ...
- 【听如子说】-python模块系列-AIS编解码Pyais
Pyais Module Introduce pyais一个简单实用的ais编解码模块 工作中需要和ais打交道,在摸鱼的过程中发现了一个牛逼的模块,对ais编解码感兴趣的可以拿项目学习一下,或者运用 ...