Python新手在作用域方面经常容易碰到的问题


Posted in Python onApril 03, 2015

通常,当我们定义了一个全局变量(好吧,我这样说是因为讲解的需要——全局变量是不好的),我们用一个函数访问它们是能被Python理解的:
 

bar = 42
def foo():
  print bar

在这里,我们在foo函数里使用了全局变量bar,然后它也如预想的能够正常运行:
 

>>> foo()
42

这样做很酷。通常,我们在使用了这个特性之后就想在所有的代码里用上它。如果像以下的例子中使用的话还是能够正常运行的:
 

bar = [42]
def foo():
  bar.append(0)
foo()
 
>>> print bar
[42, 0]

但是,如果我们把bar变一下呢:
 

>>> bar = 42
... def foo():
...   bar = 0
... foo()
... print bar
42

我们可以看到foo函数运行的好好的并且没有抛出异常,但是当我们打印bar的值的时候会发现它的值仍然是42。造成这种情况的原因就是 bar=0 这行代码,它没有改变全局变量bar的值,而是创建了一个名字也叫bar的局部变量并且它的值为0。这是个很难发现的bug,这会让没有真正理解Python作用域的新手非常痛苦。为了理解Python是如何处理局部变量和全局变量的,我们来看一种更少见的,但是可能会更让人困惑的错误,我们在打印bar的值后定义一个叫bar这个局部变量:
 

bar = 42
def foo():
  print bar
  bar = 0

这样写应该是不会出错的,不是吗?我们在打印了值之后定义了相同名称的变量,所以这应该是不会影响的(Python毕竟是一种解释型语言),真的是这样吗?

 

出错了

这怎么可能呢?好吧,这里有两处错误。第一点就是关于Python的,作为一种解释型语言(非常酷,我们都同意这一点),是一行一行地执行的。而事实上,Python是一个声明一个声明执行的。为了让你对我想表达的意思有点感觉,赶紧打开你最爱的shell,然后输入以下代码:
 

def foo():

按回车键。正如你看到的,shell里面并没有打出任何输出而是等着让你继续函数的定义。Shell里会一直这样直到你停止定义函数。这是因为定义函数是一个声明。好吧,这是一个混合的声明,里面包含了一些其他的声明,但它仍然是一个声明。直到函数被调用,不然这个函数里的内容是不会执行的。真正执行的是一个function类型的对象被创建出来了。

这引导我们来关注第二点。再强调一下,Python的动态性和解释型的特性让我们相信当 print bar 这行被执行的时候,Python会在首先在局部作用域里寻找叫bar的变量然后再去寻找全局作用域里的。但实际上发生的是局部作用域不是完全动态的。当def 这个声明执行的时候,Python会静态地从这个函数的局部作用域里获取信息。当来到 bar=0 这行的时候(不是执行到这行代码,而是当Python解释器读到这行代码的时候),它会把'bar'这个变量加入到foo函数的局部变量列表里。当foo函数执行并且Python准备执行print bar这行的时候,它就会在局部的作用域里寻找这个变量,由于这个过程是静态的,Python知道这个变量还没有被赋值,这个变量没有值,所以抛出了异常。

你可能会问:为什么不能在声明函数的时候抛出这个异常呢?Python可以知道预先知道bar这个变量在赋值前被引用了。这个问题的答案就是Python无法知道这个局部变量bar是否被赋值了。看看下面的例子:
 

bar = 42
def foo(baz):
  if baz > 0:
    print bar
  bar = 0

Python在动态和静态之间玩了一个微妙的游戏。它唯一知道的事情就是bar是被赋值了,但它不知道在赋值前被引用这个异常是否存在直到它真的发生。好吧,老实说,它根本就不知道这个变量是否被赋值!
 

bar = 42
def foo():
  print bar
  if False:
    bar = 0
 
>>> foo()
Traceback (most recent call last):
 File "<pyshell#17>", line 1, in <module>
  foo()
 File "<pyshell#16>", line 3, in foo
  print bar
UnboundLocalError: local variable 'bar' referenced before assignment

看到上面的代码里面,虽然我们作为一种智能生物能够很清楚的知道不会给bar赋值。Python无视了那个事实而是仍然声明了bar这个局部变量。

关于这个问题我已经说了够长了。我们需要的是解决方案,我会在这里给出两个解决方法。
 

>>> bar = 42
... def foo():
...   global bar
...   print bar
...   bar = 0
...
... foo()
42
>>> bar
0

第一就是使用global关键字。这是不言自明的。这会让Python知道bar是一个全局变量而不是局部变量。

第二个方法,也是更推荐使用的,就是不要使用全局变量。在我的大量Python开发工作中从来没有用到global这个关键字。能知道怎么用它就行了,但最终还是要尽量避免使用它。如果你想保存在代码里至始至终用到的值的时候,把它定义为一个类的属性。用这种方法的话就完全不需要用global了,当你要用这个值的时候,通过类的属性来访问就可以了:
 

>>> class Baz(object):
...   bar = 42
...
... def foo():
...   print Baz.bar # global
...   bar = 0 # local
...   Baz.bar = 8 # global
...   print bar
...
... foo()
... print Baz.bar
42
0
8
Python 相关文章推荐
从Python程序中访问Java类的简单示例
Apr 20 Python
Python基于Tkinter实现的记事本实例
Jun 17 Python
在DigitalOcean的服务器上部署flaskblog应用
Dec 19 Python
解析Python的缩进规则的使用
Jan 16 Python
Django REST framework 视图和路由详解
Jul 19 Python
python 并发编程 非阻塞IO模型原理解析
Aug 20 Python
python对常见数据类型的遍历解析
Aug 27 Python
kafka-python 获取topic lag值方式
Dec 23 Python
详解如何在PyCharm控制台中输出彩色文字和背景
Aug 17 Python
解决Python 写文件报错TypeError的问题
Oct 23 Python
Python Tkinter实例——模拟掷骰子
Oct 24 Python
使用pipenv管理python虚拟环境的全过程
Sep 25 Python
Python中设置变量作为默认值时容易遇到的错误
Apr 03 #Python
用Python编写一个简单的Lisp解释器的教程
Apr 03 #Python
举例讲解Python中is和id的用法
Apr 03 #Python
详解Python2.x中对Unicode编码的使用
Apr 03 #Python
对于Python中线程问题的简单讲解
Apr 03 #Python
python BeautifulSoup设置页面编码的方法
Apr 03 #Python
用Python编写一个简单的FUSE文件系统的教程
Apr 02 #Python
You might like
PHP取得一个类的属性和方法的实现代码
2011/05/22 PHP
PHP小技巧之JS和CSS优化工具Minify的使用方法
2014/05/19 PHP
Laravel统计一段时间间隔的数据方法
2019/10/09 PHP
javascript定义函数的方法
2010/12/06 Javascript
js中回调函数的学习笔记
2014/07/31 Javascript
浅谈javascript面向对象程序设计
2015/01/21 Javascript
JavaScript插件化开发教程 (四)
2015/01/27 Javascript
js实现感应鼠标图片透明度变化的方法
2015/02/20 Javascript
.NET微信公众号开发之创建自定义菜单
2015/07/16 Javascript
BootStrap 智能表单实战系列(十)自动完成组件的支持
2016/06/13 Javascript
在一个页面重复使用一个js函数的方法详解
2016/12/26 Javascript
Vue.js之slot深度复制详解
2017/03/10 Javascript
详解AngularJS1.6版本中ui-router路由中/#!/的解决方法
2017/05/22 Javascript
详解微信小程序 通过控制CSS实现view隐藏与显示
2017/05/24 Javascript
讲解vue-router之什么是动态路由
2018/05/28 Javascript
layui加载表格,绑定新增,编辑删除,查看按钮事件的例子
2019/09/06 Javascript
详解node登录接口之密码错误限制次数(含代码)
2019/10/25 Javascript
如何基于js判断浏览器版本
2020/02/20 Javascript
Vue实现图片轮播组件思路及实例解析
2020/05/11 Javascript
[53:18]Spirit vs Liquid Supermajor小组赛A组 BO3 第三场 6.2
2018/06/03 DOTA
[07:01]DOTA2-DPC中国联赛正赛 Aster vs Magma 3月5日 赛后选手采访
2021/03/11 DOTA
简单介绍Python中利用生成器实现的并发编程
2015/05/04 Python
Windows上使用virtualenv搭建Python+Flask开发环境
2016/06/07 Python
python中将\\uxxxx转换为Unicode字符串的方法
2018/09/06 Python
Python yield的用法实例分析
2020/03/06 Python
Python关键字及可变参数*args,**kw原理解析
2020/04/04 Python
软件测试题目
2013/02/27 面试题
法律工作求职自荐信
2013/10/31 职场文书
工程招投标邀请书
2014/01/30 职场文书
出纳担保书范文
2014/04/02 职场文书
希特勒的演讲稿
2014/05/23 职场文书
2014年文员工作总结
2014/11/18 职场文书
JS + HTML 罗盘式时钟的实现
2021/05/21 Javascript
mybatis 获取无数据的字段不显示的问题
2021/07/15 Java/Android
Python 使用 Frame tkraise() 方法在 Tkinter 应用程序中的Frame之间切换
2022/04/24 Python
MySQL安装失败的原因及解决步骤
2022/06/14 MySQL