Python:标识符的作用域解析

张开发
2026/4/21 1:42:18 15 分钟阅读

分享文章

Python:标识符的作用域解析
相关阅读Python专栏https://blog.csdn.net/weixin_45791458/category_12403403.html?spm1001.2014.3001.5482Python允许在标识符定义之前就“使用”它但这里说的“使用”并不是指真正意义上的立即执行而是指在语法和编译阶段Python允许你先写出对某个名字的引用只要这个名字在真正运行到那一行代码时已经能够在对应作用域中找到程序就不会报错。很多初学者刚接触这一点时往往会觉得Python像是在“向后看代码”其实更准确的理解应该是Python会先把源码编译成字节码在这个过程中确定每个名字属于哪个作用域而真正去取这个名字对应的对象则发生在代码执行到那一行的时候。先看最典型的一种情况也就是函数内部调用另一个在后面定义的函数# 例1 def a(): b() def b(): print(This is b) # 输出This is b a()例1展示了一个函数标识符在定义前就调用了这在C语言中是会报错的因为b函数并没有在a函数前定义也没有函数原型声明Python中没有函数原型。在Python中当解析到函数a内部的b调用时它会尝试在函数a中的局部作用域中查找是否定义标识符b如果没有则将其视为更高作用域中的标识符。在最后调用函数a时全局作用域已经定义了函数b因此不会报错。如果像下面这样在b还没有被创建并绑定到全局名字之前就先调用a那么就会出错# 例2 def a(): b() # NameError: name b is not defined a() def b(): print(This is b)这个例子更能说明问题。a函数的定义本身没有问题因为定义函数时函数体里的b()并不会立即执行但当顶层代码运行到a()这一句时名字b还没有绑定成功因此a内部在查找b时找不到对应对象于是抛出NameError。也就是说Python并不是允许“任意先用后定义”而是允许“先写出引用只要真正执行到引用处时这个名字已经存在就可以”。这一点不仅适用于函数名也适用于普通变量。例如下面这种写法同样是合法的# 例3 def a(): print(c) # 输出5 c5 a()原因和前面的函数例子完全一样。定义a时函数体中的print(c)不会立刻执行等到真正调用a时模块级变量c已经绑定为5了因此可以正常打印出来。很多人第一次看到这里会觉得“Python是不是运行时才决定作用域”这个说法其实只说对了一半。更准确地说Python在编译阶段就已经分析了这个名字该按什么作用域规则去找但真正取值是在运行到print(c)那一刻才发生的。这里就要说到一个非常关键的问题Python中名字的查找遵循LEGB规则也就是Local、Enclosing、Global、Builtins。也就是说Python在函数内部查找一个标识符时会先看当前函数的局部作用域再看外层嵌套函数作用域再看当前模块的全局作用域最后再看内建作用域。因此像print、len这类名字虽然你没有自己定义但因为它们存在于内建作用域中所以大多数情况下仍然能直接使用。不过需要特别注意的是虽然名字的“查找”发生在运行时但某个名字到底被当作局部变量、全局变量还是自由变量这件事往往在编译阶段就已经确定了而不是运行到那一行时才临时决定。这也是为什么下面这个例子会报错# 例4 def a(): print(c) # UnboundLocalError: local variable c referenced before assignment c1 c5 a()很多人看到这个例子时会想既然全局里已经有c5为什么print(c)不能直接打印5原因就在于Python在编译函数a的时候看到了函数体中存在c1这样的赋值语句于是它会直接把c判定为a函数的局部变量。既然c已经被认定为局部变量那么前面的print(c)就不会再去全局作用域里找c而是会尝试读取当前函数局部作用域中的c。但问题是执行到print(c)时这个局部变量还没有赋值于是就不是NameError而是UnboundLocalError。换句话说这里报错不是因为“找不到名字c”而是因为“Python已经认定你要用的是局部变量c但这个局部变量在使用前还没有绑定值”。这正是NameError和UnboundLocalError最核心的区别。NameError通常表示当前查找路径上根本没有这个名字而UnboundLocalError则表示这个名字已经被解析成了当前局部作用域中的变量只是它还没有来得及绑定值。很多初学者会把这两个异常混在一起理解其实它们恰好反映了Python作用域解析里的两个不同阶段一个是“查找不到”一个是“作用域已经确定但局部变量尚未赋值”。与此相对如果标识符对应的是一个可变对象而你只是修改它的内部元素而不是对这个名字本身重新赋值那么Python不会把它认定成局部变量。例如​# 例5 def a(): print(c) # 输出[1,2,3] c[0]4 c[1,2,3] a()这里不会报错因为函数内部并没有出现c...这样的重新绑定语句只有c[0]4这样的元素修改操作。对Python来说这表示你要使用的是外层已经存在的那个名字c对应的对象然后去修改这个对象的内容而不是在当前函数里新建一个名为c的局部变量。因此print(c)仍然会把c当作全局名字处理。这个例子说明Python判断一个名字是否是局部变量关键看的是“有没有对这个名字本身赋值”而不是“有没有通过这个名字去修改对象内容”。如果要修复例4的错误可以使用global关键词要求在编译时将标识符c视为在全局作用域中的如例6所示。# 例6 def a(): global c print(c) # 输出5 c1 c5 a()这里global c的作用不是“去全局找一个c”而是明确告诉Python在当前函数中名字c应当被视为全局作用域中的绑定而不是局部变量。这样一来前面的print(c)就会去读全局变量c后面的c1也会直接修改全局作用域中的c。也就是说global影响的是“名字归属到哪个作用域”而不是单纯改变某一次读取行为。例7展示了和例4很接近、但更容易让人误解的一种情况只不过这里换成了函数标识符# 例7 def a(): b() # UnboundLocalError: local variable b referenced before assignment def b(): print(This is b in a) def b(): print(This is b) a()很多人第一眼会觉得这里应该调用全局函数b因为全局里明明定义了一个b。但实际上不会。原因和例4完全一样Python在编译函数a时看到了内部又定义了一个def b()而函数定义语句本质上也是一种对名字b的绑定因此Python会把a中的b认定为局部名字。于是前面的b()不会再去找全局函数b而是试图调用当前局部作用域中的b可是执行到这一句时局部名字b还没有绑定成功因此就会报UnboundLocalError。这个例子非常典型因为它说明了函数名本身也同样遵循作用域解析规则并不是只有普通变量才会触发这种现象。Python在查找名字时是一层一层从内向外找的这一点在嵌套函数中会体现得很明显。例如# 例8 def a(): def b(): def c(): print(test) # 输出1 c() b() test1 a()这个例子没有问题因为在函数c内部查找test时当前局部没有外层函数b中也没有函数a中也没有于是继续去全局作用域中找最后找到test1因此可以正常输出。这个例子很好地展示了LEGB规则中的查找顺序从最内层一路向外直到找到为止。理解了这一点之后再看下面这个例子为什么出错就不难了# 例9 def a(): def b(): def c(): print(test) # NameError: free variable test referenced before assignment in enclosing scope c() test2 b() test1 a()

更多文章