基于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中numpy.zeros(np.zeros)的使用方法
Nov 07 Python
Python人脸识别初探
Dec 21 Python
Python实现的网页截图功能【PyQt4与selenium组件】
Jul 12 Python
Python安装Flask环境及简单应用示例
May 03 Python
Python Excel处理库openpyxl使用详解
May 09 Python
使用python PIL库实现简单验证码的去噪方法步骤
May 10 Python
python中eval与int的区别浅析
Aug 11 Python
python3实现高效的端口扫描
Aug 31 Python
python制作英语翻译小工具代码实例
Sep 09 Python
用python求一重积分和二重积分的例子
Dec 06 Python
python 求10个数的平均数实例
Dec 16 Python
Python获取android设备cpu和内存占用情况
Nov 15 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中的正规表达式(一)
2006/10/09 PHP
作为PHP程序员应该了解MongoDB的五件事
2013/06/03 PHP
PHP获取数组最后一个值的2种方法
2015/01/21 PHP
Laravel框架分页实现方法分析
2018/06/12 PHP
javascript实现的在当前窗口中漂浮框的代码
2010/03/15 Javascript
Javascript 面向对象 命名空间
2010/05/13 Javascript
javascript 在firebug调试时用console.log的方法
2012/05/10 Javascript
jQuery写fadeTo示例代码
2014/02/21 Javascript
详解javascript new的运行机制
2016/01/26 Javascript
JavaScript判断微信浏览器实例代码
2016/06/13 Javascript
js中创建对象的几种方式
2017/02/05 Javascript
详解webpack+angular2开发环境搭建
2017/06/28 Javascript
Angular浏览器插件Batarang介绍及使用
2018/02/07 Javascript
node.js到底要不要加分号浅析
2018/07/11 Javascript
解决Vue 项目打包后favicon无法正常显示的问题
2018/09/01 Javascript
JavaScript自动生成 年月范围 选择功能完整示例【基于jQuery插件】
2019/09/03 jQuery
Layui Form 自定义验证的实例代码
2019/09/14 Javascript
微信小程序获取当前时间及星期几的实例代码
2020/09/20 Javascript
vue 使用微信jssdk,调用微信相册上传图片功能
2020/11/13 Javascript
[50:15]VP vs Mineski 2018国际邀请赛淘汰赛BO3 第二场 8.22
2018/08/23 DOTA
python中的格式化输出用法总结
2016/07/28 Python
Python操作Sql Server 2008数据库的方法详解
2018/05/17 Python
Python Django的安装配置教程图文详解
2019/07/17 Python
Django打印出在数据库中执行的语句问题
2019/07/25 Python
什么是Python包的循环导入
2020/09/08 Python
python爬虫中采集中遇到的问题整理
2020/11/27 Python
百联网上商城:i百联
2017/01/28 全球购物
Tory Burch德国官网:美国时尚生活品牌
2018/01/03 全球购物
澳大利亚在线性感内衣商店:Fantasy Lingerie
2021/02/07 全球购物
数学教学随笔感言
2014/02/17 职场文书
升旗仪式主持词
2014/03/19 职场文书
丧事主持词大全
2014/04/02 职场文书
生产文员岗位职责
2014/04/05 职场文书
计算机专业求职信
2014/06/02 职场文书
会议欢迎词
2015/01/23 职场文书
督导岗位职责
2015/02/04 职场文书