11B: 变量的作用域

第11课有三个部分A、B、C,学习顺序不分先后。

在这节课中我们会解释一个叫做变量作用域的概念。大意就是使两个不同的变量,
在其作用域不同时,可以使用同一个名字。这将使编写和修改程序更加简单,尤其是那些庞大的程序。同时,它也是我们以后会学到的内容(递归)的一个重要组成部分。

变量作用域的示例

在这个示例中,,我们程序的一部分会有下面这个函数:

def square(x):
   value = x * x
   return value
这是一个函数用来计算一个数x的平方,例如 square(5) 会返还25。现在假设我们从程序的另一个部分调用square函数,这个部分的程序为了另一个目的也使用了value 这个变量:

# 函数体从这里开始
value = 17
fivesquared = square(5)
print(fivesquared, value)
那么问题来了,在这种情况下程序会打印出什么?假设我们不知道Python是怎样工作的,那么有两种可能:

  1. 一种可能是Python觉得两个value变量应该被“分别保存”。那么,调用square会返还25,但是value在main主体中的值会保留在17,同时我们会看见输出25 17
  2. 另一种可能是当square被执行后,它将现有的value的值改写为25, 所以我们会看到输出25 25

让我们看看到底发生了什么!

Python的运行实际上符合第一种。事实上, 每当你调用一个函数,你无法改变任何在函数外定义的变量。给变量赋值的任何语言只影响一个调用函数中的“局部”变量,它只调用函数的一个“内部”变量。Python和大多数其他语言这么做的原因是这样可以在编写大型程序、面对成千上百的函数时,让工作变得更加简单方便。具体来说,当我们定义一个新的函数时,我们可以在函数中使用任何变量名(包括常见的名称 result, tempi) 即使他们在其他地方被用于不同的目的。

详细来看。在代码展示台的第六步中,你会注意到这里有两个不同的value变量: 一个在square函数中, 另一个在函数外面(在全局中)。

选择题练习: 内部与外部
下面这个程序的输出是什么?

x = "外部"
def xReplace(value):
   x = value
xReplace("内部")
print(x)
正确! 语句x = value只会影响函数中局部类型的变量x。全局类型的 x不会改变(所以,函数 xReplace 并没有任何用处,也不会造成任何影响)。

另一个和作用域类似的概念是命名空间;命名空间就像是一个包含作用域的包裹。所以即使你import一个包裹 (例如 math),在一些地方用到变量名x,它也不会与你的程序使用的变量x重叠, 因为它们在不同的命名空间里。

作用域规则:向外展望

有些情况下,我们想混合局部和全局的变量。一个常见的例子是在程序开始时有一些常见的初始化变量,但你也希望你的函数能够读取这些全局变量。

示例
在一个函数中读取一个全局变量

在这里, 局部作用域并不含有一个叫作favouriteTopping的变量,但是这并不是造成错误的原因。Python评估变量的规则是,如果一个不在局部作用域里的变量需要被评估,那么它会在全局作用域中寻找此变量。在我们的案例中,它确实在全局中找到了favouriteTopping(在通常情况下,用一个函数体调用另一个函数你会碰到三层作用域;Python总是先检查“最局部的”,接着朝全局作用域“一步一次”的前进,直到发现这个变量。)。

上面的两个例子,orderPizzasxReplace在语法上基本相同。为什么Python在xReplace中创建了一个新的局部变量x,但没有在 orderPizzas中创建新的favouriteTopping?Python的规则如下:如果你只读取这个变量,那么没有新的局部变量会被创建;但是你哪怕只是对这个变量写入了一次,这个变量也会在函数体中被当成局部变量对待(这是造成下面这种情况的最常见原因 UnboundLocalError: 请看Python官方文档中的这两个问题。[1, 2])

函数实参forever会被当作新的局部变量,下面的代码片段说明了这个规则(它在尝试重置变量g0,但失败了)。

global改变

和其他很多事一样, 上面描述的在99%的情况下都是正确的。但是在剩下的1%情况中,你可能需要尝试在一个函数中改变一个全球变量。你可以使用Python中的global语句来完成这项任务。

这是一个对早期的xReplace示例的修改;在一个函数中,声明一个变量为global可以让你从函数内部更改全局值。

我们之前提到过, 读取一个全局变量不需要使用到global语句; 只有在写入时需要用到。这就是这节课的内容!