我并不喜欢 Python 的作用域的设计,但这门语言是如此流行,以至于很多时候你不得不去了解它. 本文试图比较全面地总结 Python 的作用域的相关规则. 若本文有错误之处,欢迎纠正.
本文基于 Python 3.6.8.
1、作用域划分
Python 中作用域的划分大致以“块”为单位. 什么是“块”呢?主要是模块、函数体、类定义(还有一些其他情况,例如函数 eval() 和 exec() 的字符串参数等). 所以,在 if / while / for 等语句中引入的变量会污染整个“块”:
def f():
... for i in range(10):
... pass
... print(i)
...
f()
9
2、名字查找
Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.
在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.
如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.
然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:
def f():
... a = 0
... def g():
... a = 1
... g()
... print(a)
...
f()
0
在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:
def f():
... a = 0
... def g():
... nonlocal a
... a = 1
... g()
... print(a)
...
f()
1
相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.
然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:
def f():
... a = 100
... def g():
... print(a)
... g()
...
f()
100
注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:
class A:
... a = 11
... def f(self):
... print(20)
... def g(self):
... print(self.a)
... self.f()def f():
... for i in range(10):
... pass
... print(i)
...
>>> f()
9
2、名字查找
Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.
在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.
如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.
然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:
>>> def f():
... a = 0
... def g():
... a = 1
... g()
... print(a)
...
>>> f()
0
在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:
>>> def f():
... a = 0
... def g():
... nonlocal a
... a = 1
... g()
... print(a)
...
>>> f()
1
相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.
然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:
>>> def f():
... a = 100
... def g():
... print(a)
... g()
...
>>> f()
100
注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:
>>> class A:
... a = 11
... def f(self):
... print(20)
... def g(self):
... print(self.a)
... self.f()
...
x = A()
x.g()
11
20
3、名字绑定的影响是“前后双向”的
如果你在一个“块”中的任何地方绑定一个名字,该“块”中所有对该名字的使用都会被当成是指向当前“块”的. 这意味着以下的代码会报“赋值前使用”的错,因为这里 print(a) 中的 a 被认为是之后的 for 循环中的 a,即使外层有一个 a 也无济于事:
def f():
... a = 100
... def g():
... print(a)
... for a in range(10):
... pass
... g()
...
f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in f
File "<stdin>", line 4, in g
UnboundLocalError: local variable 'a' referenced before assignment
这时候,只要将 for 循环中的变量改个名字,print(a) 中的 a 就会被正常的名字查找过程在外层找到:
def f():
... a = 100
... def g():
... print(a)
... for b in range(10):
... pass
... g()
...
f()
100
- Lexical Scoping or Dynamic Scoping
Python 是 lexical scoping. 比如说:
def f():
... x = 1
... def g():
... print(x)
... def h():
... x = 2
... g()
... h()
...
f()
1
虽然作用域是被静态决定的,但它们是被“动态使用”的。考虑下面的例子:
def f():
... def g():
... print(x)
... x = 1
... g()
...
f()
1
在这里,g 被定义时尚未有 x 的存在,但当 g 被调用时却可以使用 g 定义后引入的 x. 不过,g 中使用的 x 仍然属于 g 被定义时的外层作用域(即 f 的内部),从这个意义上说,此处的作用域规则仍然是 lexical scoping.
要注意的是,根据 Python 官方 Tutorial 的说法,Python 正在向“静态名字解析”方向演化,所以请勿依赖这种动态名字解析!
Python学习交流群:835017344,这里是python学习者聚集地,有大牛答疑,有资源共享!有想学习python编程的,或是转行,或是大学生,还有工作中想提升自己能力的,正在学习的小伙伴欢迎加入学习。
网友评论