在此作用域中尚未声明_Python 的作用域……

我并不喜欢 Python 的作用域的设计,但这门语言是如此流行,以至于很多时候你不得不去了解它. 本文试图比较全面地总结 Python 的作用域的相关规则.

本文基于 Python 3.6.8.

1、作用域划分

Python 中作用域的划分大致以“块”为单位. 什么是“块”呢?主要是模块、函数体、类定义(还有一些其他情况,例如函数 eval() 和 exec() 的字符串参数等). 所以,在 if / while / for 等语句中引入的变量会污染整个“块”:

>>> 

2、名字查找

Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.

在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.

如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.

然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:

>>> 

在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:

>>> 

相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.

然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:

>>> 

注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:

>>> 

3、名字绑定的影响是“前后双向”的

如果你在一个“块”中的任何地方绑定一个名字,该“块”中所有对该名字的使用都会被当成是指向当前“块”的. 这意味着以下的代码会报“赋值前使用”的错,因为这里 print(a) 中的 a 被认为是之后的 for 循环中的 a,即使外层有一个 a 也无济于事:

>>> 

这时候,只要将 for 循环中的变量改个名字,print(a) 中的 a 就会被正常的名字查找过程在外层找到:

>>> 

4. Lexical Scoping or Dynamic Scoping

Python 是 lexical scoping. 比如说:

>>> 

虽然作用域是被静态决定的,但它们是被“动态使用”的。考虑下面的例子:

>>> 

在这里,g 被定义时尚未有 x 的存在,但当 g 被调用时却可以使用 g 定义后引入的 x. 不过,g 中使用的 x 仍然属于 g 被定义时的外层作用域(即 f 的内部),从这个意义上说,此处的作用域规则仍然是 lexical scoping.

要注意的是,根据 Python 官方 Tutorial 的说法,Python 正在向“静态名字解析”方向演化,所以请勿依赖这种动态名字解析!

References

4. Execution model

7. Simple statements

9. Classes - Python 3.6.8 documentation

https://en.wikipedia.org/wiki/Scope_(computer_science)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值