1 作用域
函数在编程语言中是作为一个整体而存在,Python中的函数是一等公民的对象,于是在函数里定义的函数(嵌套函数)或者一些变量,就有其作用的范围。如下代码:
1 def fn(): 2 a = 5 3 print(a)
执行上面代码会出现NameError: name 'a' is not defined错误,原因就在于函数体中的a的作用范围只是在函数内。
1.1 局部变量和全局变量
函数的参数以及函数体中定义的所有变量,共同构成了该函数的本地命名空间,所有的变量被称为该函数的本地变量,这些变量的作用范围只是在函数中有用,所以也称其为局部变量。不是本地变量的变量被称为全局变量。
系统每次执行一个函数时,就会创建新的本地(局部)命名空间。该命名空间代表一个局部环境。解析这些名称时,解释器将首相搜索局部命名空间。如果灭有找到匹配的名称,就会搜索全局命名空间。如果在全局命名空间中也找不到匹配值,最终会检查内置命名空间。如果仍然找不到,就会抛出NameError异常。
在函数的本地变量与全局变量名称相同时,则函数体内的该名称指的是本地变量,而不是全局变量。如下示例:
1 a = 40 2 def foo(): 3 a = 13 4 foo() 5 print(a) 6 # a仍然是40
Python中赋值即定义,此时函数外部的a变量和函数体内的a变量是两个不同的变量,可以通过id看出两者指向不同的地址空间。
再看示例:
1 x = 5 2 def foo(): 3 y = x + 1 4 x += 1 5 print(x) 6 foo()
函数能执行吗?我们来分析一下:首先整个程序定义了一个全局变量x = 5,其生命周期随着整个程序的消亡而消亡;随后程序定义了foo()函数;之后有调用了foo函数,只有调用了函数,才会执行函数体内的代码。这时执行函数体内的代码。
程序来到第三行,赋值即定义,于是定义了一个y变量,然后执行等号右边的内容,对于x变量,解释器首先会在本地范围找看是否存在定义x变量,此时没有,在全局命名空间中找到x = 5,于是将x + 1的值赋给y,这是y指向的值为6;程序来到第四行,x += 1等价于x = x + 1,相当于在foo函数内部重新定义了一个局部变量(赋值即定义),那么在函数内部所有x都是该局部变量。但是该x还没有完成赋值,等号右边就拿来做加1操作,于是就会出现如下错误。
那么如何解决上面出现的问题呢?请看下面内容。
1.2 全局变量global
在默认情况下,函数体内定义(绑定)的任何变量都是该函数的本地变量。利用global关键可以将函数内部的变量声明成全局变量。示例如下:
1 x = 5 2 def foo(): 3 global x 4 x += 1 5 foo() 6 print(x)
使用global关键字的变量,将foo内的x声明为使用外部的全局作用域中定义的x,其中全局作用域中必须有x的定义,否则会出现NameError错误。如果将变量的定义以及全局的声明都放在函数体内出怎样?
1 def foo(): 2 global x 3 x = 10 4 x += 1 5 print(x) 6 foo() 7 print(x)
同样,使用global关键字的变量,将foo内的x声明为使用外部的全局作用域中定义的x。但是,x = 10赋值即定义,在内部作用域为一个外部作用域的变量x赋值,不是在内部作用域中重新定义了个新变量,所以x += 1不会报错。注意,这里x的作用域是全局的。
1.2.1 global总结
global关键字的作用是将函数内部的变量声明称全局变量。如果函数体只是要使用一个全局变量,可以不使用global。只有在函数体内重新定义(绑定)一个全局变量时,才使用global语句。如果使用了global,外部作用域变量会在内部作用域课件,但也不要在这个内部的局部作用域中使用,因为函数的目的就是为了封装,尽量与外界隔离。如果函数需要外部全局变量,请使用函数的形参传参解决。特别需要说明的是,永远都不要使用global语句,学习它只是为了深入理解变量作用域。
2 嵌套函数
2.1 自由变量与闭包
只有涉及嵌套函数时才有自由变量和闭包的问题。自由变量是指未在本地作用域中定义的变量,例如定义在内存函数外的外城函数的作用域中的变量;闭包指的是内层函数引用了外层函数的自由变量。
如下示例:
1 def counter(): 2 c = [0] 3 def inc(): 4 c[0] += 1 5 return c[0] 6 return inc 7 8 foo = counter() 9 print(foo(), foo()) 10 c = 100 11 print(foo())
代码解析:
第4行代码,由于c已经在counter函数中定义了,而且在inc函数中的使用方式是为c的元素修改值,而不是重新定义变量。于是第八行打印的结果为1 2;对于第10行打印的结果为3是因为,第9行的c和counter中的c不一样,第9行的c是全局变量,而counter中的c是counter局部的一个变量,inc函数中引用的是自由变量正是counter的变量c。该过程形成了闭包。
这种方式是Python2中实现闭包的方式,利用科列表是可变的对象这一事实。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
2.1.1 nonlocal关键字
nonlocal关键字是Python 3中引入的,其作用主要声明变量不在本地作用域定义,而是在上级的某一级局部作用域中定义,但不能是全局作用域中定义。看如下代码:
1 def counter(): 2 count = 0 3 def inc(): 4 count += 1 5 return count 6 return inc 7 8 foo = counter() 9 foo() 10 foo()
上述代码在执行过程中会出错。原因在于,对于数字、字符串、元祖等不可变类型来说,只能读取,不能更新。如果尝试重新赋值(绑定),例如count += 1,其实会隐式创建局部变count,这样,count就不是自由变量了,因此不会保存在闭包中。对上述代码进行改进如下:
1 def counter(): 2 count = 0 3 def inc(): 4 nonlocal count 5 count += 1 6 return count 7 return inc 8 9 foo = counter() 10 foo() 11 foo()
该代码可以正常执行,因为在inc函数中使用nonlocal关键字声明count变量在上级作用域而非本地作用域中定义,形成了闭包。再看如下代码:
1 a = 50 2 def counter(): 3 nonlocal a 4 a += 1 5 print(a) 6 count = 0 7 def inc(): 8 nonlocal count 9 count += 1 10 return count 11 return inc 12 13 foo = counter() 14 foo() 15 foo()
该代码不能正常执行,因为在counter函数中,声明a变量不在本地作用域定,nonlocal就会向上级去找变量a的定义,但是counter的上级就是全局变量a,于是就报错。
3 默认值作用域
在函数的参数列表中,有时候会设置一些位置参数或keword-only参数的默认值(缺省值),这样保证在调用函数如果没有实参传入时,函数也能正常的执行。那么这些默认值的作用范围是什么情况呢?下面通过代码来说明。
3.1 不可变类型作为默认值
下面来看下,函数参数的默认值为字符串、数值型、元组这些不可变类型的情况。代码如下:
1 def foo(w, u='abc', *, z=123): 2 u = 'xyZ' 3 z = 789 4 print(w, u, z) 5 6 print(foo.__defaults__) 7 foo('viktor') 8 print(foo.__defaults__)
9 print(foo.__kwdefaults__)
函数属性__defaults__中将所有位置参数的默认值以元组的形式保存,这样它就不会因为在函数体内使用了它而改变;对于keyword-only参数,属性__kwdefaults__使用字典保存所有keyword-only参数的默认值。
由上面的代码可知,对于位置参数的默认值,Python使用元组的形式将其保存在__defaults__属性中;keyword-only参数使用字典的形式将其保存在__kwdefaults__中。如果,参数的默认值为不可变类型时,在函数体内使用了该参数,不会改变属性中对应的参数值。那么如果参数的默认值为可变类型的呢,还是和上面的情况类似呢?
3.2 可变类型作为默认值
直接上代码:
1 def foo(w, u='abc', *, z=123, zz=[456]): 2 u = 'xyz' 3 z = 789 4 zz.append(1) 5 print(w, u, z, zz) 6 print(foo.__defaults__) 7 foo('viktor') 8 print(foo.__defaults__) 9 print(foo.__kwdefaults__)
通过上面的代码执行的结果可知,使用可变类型作为默认值,可能修改该默认值,如果再次调用函数,没有给出对应的实参时,则会影响函数执行的结果。有时候这个特性是好的,有时候这种特性是不好的,有副作用。那么如何按需改变呢?看下面的两种方法。
方法1:
1 def foo(xyz=[], u='abc', z=123): 2 xyz = xyz[:] # 影子拷贝 3 xyz.append(1) 4 print(xyz) 5 foo() 6 print(foo.__defaults__) 7 foo() 8 print(foo.__defaults__) 9 foo([10]) 10 print(foo.__defaults__) 11 foo([10,5]) 12 print(foo.__defaults__)
使用影子拷贝后,此时函数体内的xyz是传入参数或者默认参数的副本,在函数体内的对xyz的操作已经不再是对形参的xyz进行操作。这样就保证了不修改参数的默认值。
方法2:
1 def foo(xyz=None, u='abc', z=123): 2 if xyz is None: 3 xyz = [] 4 xyz.append(1) 5 print(xyz) 6 foo() 7 print(foo.__defaults__) 8 foo() 9 print(foo.__defaults__) 10 foo([10]) 11 print(foo.__defaults__) 12 foo([10,5]) 13 print(foo.__defaults__)
方法2中使用不可变类型默认值,在函数体内如果使用缺省值None就创建一个列表,这时,调用函数时,该位置参数接收的是一个列表,就修改这个列表,不会改变默认值。这种方式灵活,应用广泛,很多函数的定义,都可以看到使用None这个不可变得值作为默认参数,可以说这时一种惯用法。