伯克利CS 61A的教学用书笔记和一些课程笔记
2.4.4 Local State
Lists and dictionaries have local state: they are changing values that have some particular contents at any point in the execution of a program. The word "state" implies an evolving process in which that state may change.
列表(list)和字典(dictionary)都有local state,程序执行的任何时候都可以改变他们的特定内容。
函数也有local state,比如我们定义函数withdraw,用来模拟从银行账户中取款,输入参数表示取出的钱,输出还剩下的钱,如果取出的钱超过了当前账户的钱,就输出 'Insufficient funds',假设我们一开始有100元。
>>> withdraw(25)
75
>>> withdraw(25)
50
>>> withdraw(60)
'Insufficient funds'
>>> withdraw(15)
35
在上面的过程中,我们可以看到,withdraw(25)被调用了两次,但是得到了不同的结果(the expression withdraw(25), evaluated twice, yields different values.),这是因为我们定义的这个函数是 ' 非纯 ' 的(Thus, this user-defined function is non-pure.),调用的函数不仅仅返回一个值,还有其他的副作用,所以下次调用会返回不同的值(Calling the function not only returns a value, but also has the side effect of changing the function in some way, so that the next call with the same argument will return a different result. ),这个副作用就是 withdraw函数在当前的环境之外改变name-value的绑定,也就是说在当前环境中改变了函数的局部域中的值。
为了使withdraw有意义,我们必须创建一个初始账户值,make_withdraw是一个高阶函数(higher-order function),以账户初始值为参数,函数withdraw是它的返回值。
>>> withdraw = make_withdraw(100)
make_withdraw的实现需要一种新的语句:nonlocal 。下面我们来定义它。看代码可以对这个问题一目了然。
>>> def make_withdraw(balance):
"""Return a withdraw function that draws down balance with each call."""
def withdraw(amount):
nonlocal balance # Declare the name "balance" nonlocal
if amount > balance:
return 'Insufficient funds'
balance = balance - amount # Re-bind the existing balance name
return balance
return withdraw
如果没有nonlocal,在其他作用域中无法更改balance,如果有nonlocal,那么我们在其他地方改变它,它也会在第一次定义它的作用域中被更改,也就是make_withdraw。参考下图。
上图中,每次调用wd时,也就是调用了withdraw函数,它都改变了withdraw函数的父环境make_withdraw函数中的变量balance(python不允许子环境改变父环境中的值),这在通常情况下是不允许的,但是有local,就可以(The nonlocal statement allows withdraw to change a name binding in the make_withdraw frame.)。下面我们来聊聊关于nonlocal的细节。
在当前环境中声明了balance为nonlocal后,在当前环境用赋值语句 = 更改balance都不会在当前环境中绑定balance,而是找到第一个定义balance的环境,并在该环境中重新绑定balance的值。所以如果balance事先没有赋初值,而在子环境中声明nonlocal balance并更改了balance,那么nonlocal将会抛出一个错误。(The nonlocal statement changes all of the remaining assignment statements in the definition of withdraw. After executing nonlocal balance, any assignment statement with balance on the left-hand side of = will not bind balance in the first frame of the current environment. Instead, it will find the first frame in which balance was already defined and re-bind the name in that frame. If balance has not previously been bound to a value, then the nonlocal statement will give an error.)
我们第一次接触嵌套函数时,就介绍过,局部定义的函数(在一个函数中定义的函数)可以查找它的父环境中的变量,比如withdraw可以查找make_withdarw中的变量。访问非本地变量不需要nonlocal,但是要想更改非本地变量,就必须用nonlocal。(Ever since we first encountered nested def statements, we have observed that a locally defined function can look up names outside of its local frames. No nonlocal statement is required to access a non-local name. By contrast, only after a nonlocal statement can a function change the binding of names in these frames.)
其实赋值语句本身就有一种双重作用(dual role):它可以创建一个新的变量,也可以重新绑定已经存在的变量(也就是更改已经存在的变量的值)。赋值语句也能改变list或者dictionary的内容。python中赋值语句的太多作用可能会使执行赋值语句的效果模糊不清(The many roles of Python assignment can obscure the effects of executing an assignment statement.)。作为一名程序员,你应该清楚地记录(document)你的代码,以便其他人能够理解赋值的效果。
Python Particulars
python的这种非局部的赋值模式是有高阶函数和词法作用域的编程语言的一般特性(This pattern of non-local assignment is a general feature of programming languages with higher-order functions and lexical scope.)。大多数其他语言都不需要nonlocal声明,而是一种默认行为,也就是子环境改变父环境中的变量在很多语言当中是默认的。
python对变量名的查找也有一个不寻常的限制:在函数体中,一个变量名的所有实例都必须在相同作用域中被引用(within the body of a function, all instances of a name must refer to the same frame.)。此限制允许Python在执行函数主体之前预先计算包含每个名称的环境(作用域)。参考下图。
删掉nonlocal语句之后,报错显示我们局部变量 balance 在赋值之前被使用。这是因为balance在第5行被赋值,所以python假设所有对balance的引用都出现在这个局部域中,但是在第3行之前却没找到balance的赋值语句,所以报错了。如果没有nonlocal,一但在子环境(withdraw函数体中)对balance赋值,也就是更改了balance的值,那么python就认为balance是在子环境中被定义的。假设一种情况,父环境(make_withdraw)和子环境(withdraw)都定义了相同变量名balance,那么在子环境中对balance进行赋值时,python该认为你想赋值的是哪个环境中的balance呢?python会认为是想对当前环境(子环境)的balance进行赋值。后续学习解释器时,我们会看到在执行函数体之前对它进行预计算是很常见的。
2.4.5 The Benefits of Non-Local Assignment
理解nonlocal是理解对象的重要的一步,对象彼此交互,但是各自管理自己的内部状态。非局部赋值(non-local assignment)使我们能够维护函数的一些局部的状态,而且可以随着函数的调用而改变。对上面的withdraw函数来说,balance在对该函数的所有调用中被共享,但是对整个程序的其他部分确实不可访问的。另外,如果再次调用make_withdraw函数,那么就会产生一个新的独立的frame(直译是框架,不知道该翻译成什么,暂时译为环境,以前的文章以及上文中我将frame翻译为环境或者作用域),绑定另外一个新的balance。参考下图,方便理解。
上图右边的f1和f2是两个不同的frame, 相互独立,他们也绑定(bind)了各自的withdraw函数,彼此对balance的计算是相互独立的。这样每个withdraw就维持了自己的局部状态,互相不可访问。
用更高层次的思想来看这种情况,我们以及创建了一个银行账户的抽象,每个账户管理自己的内部,有自己的balance,而他们的行为确实一样的:根据提款记录来对balance进行更改。
2.4.6 The Cost of Non-Local Assignment
考虑下面的例子。
wd赋值给了wd2,从右边可以看出,他们有相同的父环境f1(也就是同一个make_withdraw),而wd和wd2都指向同一个withdraw,所以不管对谁进行调用,都会对同一个balance产生影响。
正确分析带有非局部赋值(non-local)代码的关键是,记住只有函数调用才会引入新的frame。赋值语句总是有更改现有frame中绑定变量。在本例中,除非调用了两次make_withdraw,否则balance只能有一个绑定(bind)。
Sameness and change
之所以会出现这些差别,是因为我们引入了可改变非局部环境的非纯函数,我们改变了表达式的性质。仅仅包含纯函数调用的表达式是引用透明的(referentially transparent)。使用具有局部状态的函数,我们能够实现可变数据类型。我们可以实现与上面介绍的内置list和dict类型等价的抽象数据类型。
下一篇讲迭代器和生成器。