基于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的面向对象思想分析
Jan 14 Python
go语言计算两个时间的时间差方法
Mar 13 Python
Python 3.x 新特性及10大变化
Jun 12 Python
实践Python的爬虫框架Scrapy来抓取豆瓣电影TOP250
Jan 20 Python
python中import学习备忘笔记
Jan 24 Python
Python简单定义与使用二叉树示例
May 11 Python
Django框架自定义session处理操作示例
May 27 Python
Python队列、进程间通信、线程案例
Oct 25 Python
python ftplib模块使用代码实例
Dec 31 Python
使用TensorFlow搭建一个全连接神经网络教程
Feb 06 Python
Python matplotlib实时画图案例
Apr 23 Python
python中entry用法讲解
Dec 04 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之数据库操作详解及乱码解决!
2007/01/02 PHP
php中的注释、变量、数组、常量、函数应用介绍
2012/11/16 PHP
解决thinkPHP 5 nginx 部署时,只跳转首页的问题
2019/10/16 PHP
JavaScript中的Screen屏幕对象
2008/01/16 Javascript
使用jQuery validate 验证注册表单实例演示
2013/03/25 Javascript
JS小功能(操作Table--动态添加删除表格及数据)实现代码
2013/11/28 Javascript
jquery实现瀑布流效果分享
2014/03/26 Javascript
jquery实现的导航固定效果
2014/04/28 Javascript
JS模拟实现ECMAScript5新增的数组方法
2017/03/20 Javascript
JavaScript实现简单评论功能
2017/08/17 Javascript
JS实现颜色的10进制转化成rgba格式的方法
2017/09/04 Javascript
javascript使用正则实现去掉字符串前面的所有0
2018/07/23 Javascript
使用electron将vue-cli项目打包成exe的方法
2018/09/29 Javascript
vue 组件销毁并重置的实现
2020/01/13 Javascript
VueCli生产环境打包部署跨域失败的解决
2020/11/13 Javascript
[02:01]BBC DOTA2国际邀请赛每日综述:八强胜者组鏖战,中国队喜忧参半
2014/07/19 DOTA
Python中音频处理库pydub的使用教程
2017/06/07 Python
python简单商城购物车实例代码
2018/03/15 Python
Python结合ImageMagick实现多张图片合并为一个pdf文件的方法
2018/04/24 Python
python3中os.path模块下常用的用法总结【推荐】
2018/09/16 Python
python3.6.3转化为win-exe文件发布的方法
2018/10/31 Python
python random从集合中随机选择元素的方法
2019/01/23 Python
HTML5拖拽的简单实例
2016/05/30 HTML / CSS
意大利和国际奢侈品牌购物网站:Suitnegozi.com
2021/01/15 全球购物
清洁工岗位职责
2014/01/29 职场文书
两只小狮子教学反思
2014/02/05 职场文书
竞选学委演讲稿
2014/09/13 职场文书
房屋买卖委托书格式范本格式
2014/10/13 职场文书
2014个人年终工作总结范文
2014/12/15 职场文书
毕业生班级鉴定评语
2015/01/04 职场文书
中学教师个人总结
2015/02/10 职场文书
2015年七一建党节慰问信
2015/03/23 职场文书
幼儿园老师工作总结2015
2015/05/22 职场文书
婚礼上证婚人致辞
2015/07/28 职场文书
小学四年级班务总结该怎么写?
2019/08/16 职场文书
解决pytorch-gpu 安装失败的记录
2021/05/24 Python