基于Python函数的作用域规则和闭包(详解)


Posted in Python onNovember 29, 2017

作用域规则

命名空间是从名称到对象的映射,Python中主要是通过字典实现的,主要有以下几个命名空间:

内置命名空间,包含一些内置函数和内置异常的名称,在Python解释器启动时创建,一直保存到解释器退出。内置命名实际上存在于一个叫__builtins__的模块中,可以通过globals()['__builtins__'].__dict__查看其中的内置函数和内置异常。

全局命名空间,在读入函数所在的模块时创建,通常情况下,模块命名空间也会一直保存到解释器退出。可以通过内置函数globals()查看。

局部命名空间,在函数调用时创建,其中包含函数参数的名称和函数体内赋值的变量名称。在函数返回或者引发了一个函数内部没有处理的异常时删除,每个递归调用有它们自己的局部命名空间。可以通过内置函数locals()查看。

python解析变量名的时候,首先搜索局部命名空间。如果没有找到匹配的名称,它就会搜索全局命名空间。如果解释器在全局命名空间中也找不到匹配值,最终会检查内置命名空间。如果仍然找不到,就会引发NameError异常。

不同命名空间内的名称绝对没有任何关系,比如:

a = 42
def foo():
  a = 13
  print "globals: %s" % globals()
  print "locals: %s" % locals()
  return a
foo()
print "a: %d" % a

结果:

globals: {'a': 42, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\\Users\\h\\Desktop\\test4.py', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000002C17AC8>, '__doc__': None}
locals: {'a': 13}
a: 42

可见在函数中对变量a赋值会在局部作用域中创建一个新的局部变量a,外部具有相同命名的那个全局变量a不会改变。

在Python中赋值操作总是在最里层的作用域,赋值不会复制数据,只是将命名绑定到对象。删除也是如此,比如在函数中运行del a,也只是从局部命名空间中删除局部变量a,全局变量a不会发生任何改变。

如果使用局部变量时还没有给它赋值,就会引发UnboundLocalError异常:

a = 42
def foo():
  a += 1
  return a
foo()

上述函数中定义了一个局部变量a,赋值语句a += 1会尝试在a赋值之前读取它的值,但全局变量a是不会给局部变量a赋值的。

要想在局部命名空间中对全局变量进行操作,可以使用global语句,global语句明确地将变量声明为属于全局命名空间:

a = 42
def foo():
  global a
  a = 13
  print "globals: %s" % globals()
  print "locals: %s" % locals()
  return a
foo()
print "a: %d" % a

输出:

globals: {'a': 13, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\\Users\\h\\Desktop\\test4.py', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000002B87AC8>, '__doc__': None}
locals: {}
a: 13

可见全局变量a发生了改变。

Python支持嵌套函数(闭包),但python 2只支持在最里层的作用域和全局命名空间中给变量重新赋值,内部函数是不可以对外部函数中的局部变量重新赋值的,比如:

def countdown(start):
  n = start
  def display():
    print n
  def decrement():
    n -= 1
  while n > 0:
    display()
    decrement()
countdown(10)

运行会报UnboundLocalError异常,python 2中,解决这个问题的方法是把变量放到列表或字典中:

def countdown(start):
  alist = []
  alist.append(start)
  def display():
    print alist[0]
  def decrement():
    alist[0] -= 1
  while alist[0] > 0:
    display()
    decrement()
countdown(10)

在python 3中可以使用nonlocal语句解决这个问题,nonlocal语句会搜索当前调用栈中的下一层函数的定义。:

def countdown(start):
  n = start
  def display():
    print n
  def decrement():
    nonlocal n
    n -= 1
  while n > 0:
    display()
    decrement()
countdown(10)

闭包

闭包(closure)是函数式编程的重要的语法结构,Python也支持这一特性,举例一个嵌套函数:

def foo():
  x = 12
  def bar():
    print x
  return bar
foo()()

输出:12

可以看到内嵌函数可以访问外部函数定义的作用域中的变量,事实上内嵌函数解析名称时首先检查局部作用域,然后从最内层调用函数的作用域开始,搜索所有调用函数的作用域,它们包含非局部但也非全局的命名。

组成函数的语句和语句的执行环境打包在一起,得到的对象就称为闭包。在嵌套函数中,闭包将捕捉内部函数执行所需要的整个环境。

python函数的code对象,或者说字节码中有两个和闭包有关的对象:

co_cellvars: 是一个元组,包含嵌套的函数所引用的局部变量的名字
co_freevars: 是一个元组,保存使用了的外层作用域中的变量名

再看下上面的嵌套函数:

>>> def foo():
    x = 12
    def bar():
      return x
    return bar
 
>>> foo.func_code.co_cellvars
('x',)
>>> bar = foo()
>>> bar.func_code.co_freevars
('x',)

可以看出外层函数的code对象的co_cellvars保存了内部嵌套函数需要引用的变量的名字,而内层嵌套函数的code对象的co_freevars保存了需要引用外部函数作用域中的变量名字。

在函数编译过程中内部函数会有一个闭包的特殊属性__closure__(func_closure)。__closure__属性是一个由cell对象组成的元组,包含了由多个作用域引用的变量:

>>> bar.func_closure
(<cell at 0x0000000003512C78: int object at 0x0000000000645D80>,)

若要查看闭包中变量的内容:

>>> bar.func_closure[0].cell_contents
12

如果内部函数中不包含对外部函数变量的引用时,__closure__属性是不存在的:

>>> def foo():
    x = 12
    def bar():
      pass
    return bar
 
>>> bar = foo()
>>> print bar.func_closure
None

当把函数当作对象传递给另外一个函数做参数时,再结合闭包和嵌套函数,然后返回一个函数当做返回结果,就是python装饰器的应用啦。

延迟绑定

需要注意的一点是,python函数的作用域是由代码决定的,也就是静态的,但它们的使用是动态的,是在执行时确定的。

>>> def foo(n):
    return n * i
 
>>> fs = [foo for i in range(4)]
>>> print fs[0](1)

当你期待结果是0的时候,结果却是3。

这是因为只有在函数foo被执行的时候才会搜索变量i的值, 由于循环已结束, i指向最终值3, 所以都会得到相同的结果。

在闭包中也存在相同的问题:

def foo():
  fs = []
  for i in range(4):
    fs.append(lambda x: x*i)
  return fs
for f in foo():
  print f(1)

返回:

解决方法,一个是为函数参数设置默认值:

>>> fs = [lambda x, i=i: x * i for i in range(4)]
>>> for f in fs:
    print f(1)

另外就是使用闭包了:

>>> def foo(i):
    return lambda x: x * i
 
>>> fs = [foo(i) for i in range(4)]
>>> for f in fs:
    print f(1)

或者:

>>> for f in map(lambda i: lambda x: i*x, range(4)):
    print f(1)

使用闭包就很类似于偏函数了,也可以使用偏函数:

>>> fs = [functools.partial(lambda x, i: x * i, i) for i in range(4)]
>>> for f in fs:
    print f(1)

这样自由变量i都会优先绑定到闭包函数上。

以上这篇基于Python函数的作用域规则和闭包(详解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python随机读取文件实现实例
May 25 Python
初探TensorFLow从文件读取图片的四种方式
Feb 06 Python
Django REST framework视图的用法
Jan 16 Python
Python定时任务随机时间执行的实现方法
Aug 14 Python
Python操作qml对象过程详解
Sep 26 Python
Python模块相关知识点小结
Mar 09 Python
python如何判断IP地址合法性
Apr 05 Python
基于Keras中Conv1D和Conv2D的区别说明
Jun 19 Python
在Python3.74+PyCharm2020.1 x64中安装使用Kivy的详细教程
Aug 07 Python
python中pickle模块浅析
Dec 29 Python
python常见的占位符总结及用法
Jul 02 Python
Python使用MapReduce进行简单的销售统计
Apr 22 Python
JSONLINT:python的json数据验证库实例解析
Nov 28 #Python
详解如何使用Python编写vim插件
Nov 28 #Python
从头学Python之编写可执行的.py文件
Nov 28 #Python
浅谈用Python实现一个大数据搜索引擎
Nov 28 #Python
Python中用psycopg2模块操作PostgreSQL方法
Nov 28 #Python
Python搜索引擎实现原理和方法
Nov 27 #Python
python输入错误密码用户锁定实现方法
Nov 27 #Python
You might like
PHP多维数组遍历方法(2种实现方法)
2015/12/10 PHP
php微信公众号开发(2)百度BAE搭建和数据库使用
2016/12/15 PHP
PHP如何获取当前主机、域名、网址、路径、端口等参数
2017/06/09 PHP
php使用ftp实现文件上传与下载功能
2017/07/21 PHP
PHP实践教程之过滤、验证、转义与密码详解
2017/07/24 PHP
麦鸡的TAB切换功能结合了javascript和css
2007/12/17 Javascript
用JavaScript仿PS里的羽化效果代码
2011/12/20 Javascript
基于JQuery实现的图片自动进行缩放和裁剪处理
2014/01/31 Javascript
extjs 分页使用jsp传递数据示例
2014/07/29 Javascript
JavaScript将数组转换成CSV格式的方法
2015/03/19 Javascript
js实现数组转换成json
2015/06/26 Javascript
js中 计算两个日期间的工作日的简单实例
2016/08/08 Javascript
JS动态加载脚本并执行回调操作
2016/08/24 Javascript
基于BootStrap与jQuery.validate实现表单提交校验功能
2016/12/22 Javascript
前端开发不得不知的10个最佳ES6特性
2017/08/30 Javascript
JS将网址url转化为JSON格式的方法
2018/07/02 Javascript
jQuery实现table表格checkbox全选的方法分析
2018/07/04 jQuery
vue 项目中使用Loading组件的示例代码
2018/08/31 Javascript
vue中@change兼容问题详解
2019/10/25 Javascript
vue中 数字相加为字串转化为数值的例子
2019/11/07 Javascript
uniapp 仿微信的右边下拉选择弹出框的实现代码
2020/07/12 Javascript
原生js实现简单轮播图
2020/10/26 Javascript
JavaScript实现表单验证功能
2020/12/09 Javascript
[47:46]完美世界DOTA2联赛 Magma vs GXR 第三场 11.07
2020/11/10 DOTA
python实现2014火车票查询代码分享
2014/01/10 Python
python matplotlib 画dataframe的时间序列图实例
2019/11/20 Python
Pycharm 2020最新永久激活码(附最新激活码和插件)
2020/09/17 Python
python实现逆滤波与维纳滤波示例
2020/02/26 Python
解决keras backend 越跑越慢问题
2020/06/18 Python
Python logging自定义字段输出及打印颜色
2020/11/30 Python
HTML5手机端弹出遮罩菜单特效代码
2016/01/27 HTML / CSS
Nisbets爱尔兰:英国最大的厨房和餐饮设备供应商
2019/01/26 全球购物
Vans(范斯)新西兰官方网站:美国原创极限运动品牌
2020/09/19 全球购物
函授毕业生自我鉴定
2013/11/06 职场文书
货车司机岗位职责
2014/03/18 职场文书
会议欢迎标语
2014/06/30 职场文书