:
x=4
def f2():
def f3():
nonlocal x # f1()的本地
print("f3:",x) # 输出4
x=6 # 修改f1()的本地
f3()
print("f2:",x) # 输出6
f2()
print("f1:",x) # 输出6
f1()
注意,上面f3()中的nonlocal将x修饰为f1()的本地变量,因为f3()的上一层f2()中没有变量x,所以f2()继承了f1()的变量x,使得f3()修改上一层f2()中的变量,等价于修改f1()中的变量x。
但如果把f1()中的x=4
也删除,那么将报错,因为nonlocal无法将变量修饰为全局范围。
所以,nonlocal默认将内层函数中的变量修饰为上一层函数的作用域范围,如果上一层函数中不存在该变量,则修饰为上上层、上上上层直到顶层函数,但不能修饰为全局作用域范围。
同样的,只要在内层函数中赋值,就表示声明这个变量的作用域为内层函数作用域范围。所以,下面的代码是错误的:
x=3
def f1():
x=4
def f2():
print(x)
x=3
f2()
f1()
下面的代码也是错的:
x=3
def f1():
x=4
def f2():
x += 3
print(x)
f2()
f1()
错误信息:
UnboundLocalError: local variable 'x' referenced before assignment
至于原因,前文已经解释的很清楚。
访问外层函数变量的其它方法
在以前的版本中,还没有nonlocal关键字,这时如果要保存外层函数的变量,就需要使用函数参数默认值的方式定义内层函数。
x=3
def f1():
x=4
def f2(x=x):
x += 3
print("f2:",x)
x=5
f2()
print("f1:",x)
f1()
输出:
f2: 7
f1: 5
上面的f2(x=x)
中,等号右边的x来自于f1()中x=4
,然后将其赋值给f2()的本地作用域变量x。注意,python的作用域是词法作用域,函数区块的定义位置决定了它看到的变量。所以,尽管调用f2()之前再次对x进行了赋值,f2()函数调用时,f2(x=x)
等号右边的x早已经赋值给左边的本地变量x了。它们的关系如下图所示:
避免函数嵌套
一般来说,函数嵌套都只用于闭包(工厂函数),而且是结合匿名函数(lambda)实现的闭包。其它时候,函数嵌套一般都可以改写为非嵌套模式。
例如,下面的嵌套函数:
def f1():
x=3
def f2():
nonlocal x
print(x)
f2()
f1()
可以改写为:
def f1():
x=3
f2(x)
def f2(x):
print(x)
f1()
循环内部的函数
当函数位于循环结构中,且这个函数引用了循环控制变量,那么结果可能会出乎意料。
本来以匿名函数(lambda)来解释更清晰,但因为尚未介绍匿名函数,所以这里采用命名函数为例。
下面的代码中,将5个函数作为列表的元素保存到列表list1中。
def f1():
list1 = []
for i in range(5):
def n(x):
return i+x
list1.append(n)
return list1
mylist = f1()
for i in mylist: print(i)
print(mylist[0](2))
print(mylist[2](2))
结果:
<function f1.<locals>.n at 0x02F93660>
<function f1.<locals>.n at 0x02F934B0>
<function f1.<locals>.n at 0x02F936A8>
<function f1.<locals>.n at 0x02F93738>
<function f1.<locals>.n at 0x02F93780>
6
6
从结果中可以看到mylist[0](2)
和mylist[2](2)
的执行结果是一样的,不仅如此,mylist[N](2)
的结果也全都一样。换句话说,保存到列表中的各个函数n()中所引用的循环控制变量"i"并没有因为循环的迭代而改变,而且列表中所有函数保存的i的值都是循环的最后一个元素i=4
。
(注:对于此现象,各语言基本都是如此,本节稍作解释,真正的本质原因在本文的最后一节做了额外的补充解释代码块细述)。
先看下面的例子:
def f1():
for i in range(5):
def n():
print(i)
return n
f1()()
结果输出4。可见,print(i)
的值并没有随循环的迭代过程而改变。
究其原因,是因为def n()
只是函数的声明,它不会去查找i的值是多少,所以不会将i的值替换到函数n()的i变量,而是直接保存变量i的地址,当循环结束时,i指向最后一个元素i=4的地址。
当开始调用n()的时候,即f1()()
,才会真正开始查找i的值,这时候i指向的正是i=4。
这就像下面的代码一样,在还没有开始调用f()的时候,f()内部的x一直都只是指向它所看见的变量x,而这个x是全局作用域范围。当真正开始调用f()的时候,才会去定位x的指向。
x=3
def f():
print(x)
回到上面循环中的嵌套函数,如果要保证循环的迭代能作用到其内部的函数中,可以采用默认参数值的方式进行赋值:
def f1():
list1 = []
for i in range(5):
def n(x,i=i):
return i+x
list1.append(n)
return list1
上面def n(x,i=i)
中的i=i
是设置默认参数值,等号右边的i是函数声明时就查找并替换完成的,所以每次循环迭代过程中,等号右边的i都不同,等号左边的参数i的默认值就不同。
再述作用域规则
python的作用域是词法作用域,这意味着函数的定义位置决定了它所看见的变量。除了词法作用域,还有动态作用域,动态作用域意味着函数的调用位置决定了它所看见的变量。关于词法、动态作用域,本文不多做解释,想要了解的话,可以参考一文搞懂:词法作用域、动态作用域、回调函数、闭包
下面是本文开头的问题:
x=1
def f():
x=3
g()
print("f:",x) # 3
def g():
print("g:",x) # 1
f()
print("main:",x) # 1
对于python的这段代码来说,这里有两个值得注意的地方:
- 调用函数之前,理论上要先定义好函数,但这里g()的调用似乎看上去比g()的定义更先
- f()中调用g()时,为什么g()输出的是1而不是3
第一个问题在前文已经解释过了,再解释一遍:虽然f()里面有g()的调用语句,但def f()
只是声明,但在调用f()之前,是不会去调用g()的。所以,只要f()的调用语句在def g()
之后,就是正确的。
第二个问题,python是词