python name binding and resolution
这篇主要是翻译了一些官方文档The Python Language Reference第四章的干货, 加入一些自己理解的形象化描述以及一些例子代码. 写这篇的原因是工作原因做了一个从把VBScript转换成Python的工具, 需要复习一遍python的一些基础知识.
干货
- 七种代码块
- 七种绑定方法
- 一旦在代码块中给名字绑定对象,
那么该代码块中出现的这个名字(即便在绑定之前)都被视为是本地变量.
除非用
global
或者nonlocal
解除这种限制. - 如果名字出现的代码块里没有绑定该名字, 则逐层到外层代码块中寻找绑定.
- 有一个例外, 就是类, 在类方法中使用的没有绑定的变量, 将直接到全局名字空间中查找, 而不是到最接近的外层代码块(类定义本身的名字空间)查找. 类定义的名字空间将变为类的属性字典
4. 执行模型 (Execution model)
4.1. 程序的结构
概念: 代码块 (Code Block)
代码块是指能够作为执行单元的python的程序片段. Python程序是由代码块构建的. 具体的讲, 只有下面列出的这七种代码块:
- 模块 module (也叫顶级代码块,top level code block),
包括通过解释器参数
-m
执行作为顶级脚本的模块(即__main__
模块) - 函数体
- 类定义
- 在交互模式下输入的每一个命令
- 输入给解释器的脚本文件(script file)
- 通过解释器参数
-c
输入给解释器的脚本命令(script command) - 输入给
eval()
和exec()
的字符串参数里的代码.
4.2. 名字和名字的绑定(Naming and binding)
4.2.1 名字的绑定(Binding or names)
概念: 名字, 绑定
python的一个基本抽象是对象(object), python里除了名字以外的所有的东西都是一种对象. 名字就是对象的引用. 代码里的每个名字是通过一个叫名字绑定操作的代码行为导入的.
{% blockquote %} 可以把名字理解为给python里的对象贴上的标签. 贴标签这个动作就叫做名字的绑定. {% endblockquote %}
python里所有的名字绑定操作也只有下面这七种:
- 函数形参
import
语句- 类和函数的定义(绑定类或者函数名字到定义的代码块对象)
- 赋值语句, 当赋值目标如果是一个标识符(identifier)
for
语句里的目标(包括推导式(comprehension))with
语句或者except
语句里的as
.from ... import *
语句将绑定除了以下划线开头的所有导入模块的名字. (仅能在顶级代码块中使用)
反向操作是del
语句.
{% quote %} del
语句相当于把标签从对象上撕下来. {%
endquote %}
概念: 变量(variable), 本地变量, 全局变量, 自由变量
名字也叫做变量,
绑定也可以叫做对变量的定义(define).
如果一个名字在一个代码块中被绑定,
除非被声明为nonlocal
或者global
,
这个名字将成为代码块的本地变量(local variable).
那么发生在一个代码块中的名字绑定也叫做一个变量在该代码块中被定义.
模块(module)代码块中绑定的变量既是顶级代码块的本地变量也可以叫做全局变量(global
variable). 如果一个变量在代码块中被用到但是没有在该代码块中被定义,
这个变量被叫做自由变量(free variable).
一段程序脚本中出现的每一个名字将使用下面的解析规则来确定该名字的绑定对象.
4.2.2. 名字的解析 resolution of names
{% quote %} 名字的解析的意思就是通过一个标准的过程分析出在程序代码中出现的任何一个名字所引用的对象. {% endquote %}
概念: 变量的可见范围(scope).
可见范围定义了在一个代码块里一个名字的可见性.
{% quote %} 我理解, 所有使用一个名字能够正确被解析的语句可以被理解为看见这个名字, 即这个名字对于该语句是可见的. 所以可见范围可以理解为, 所有可以看见这个名字的代码. {% endquote %}
如果一个代码块内定义了一个本地变量, 那么这个变量的可见范围就是这个代码块. 如果定义发生在一个函数代码块中, 可见范围会包括被包含在该函数代码块中的其他代码块, 除非里面的代码块重新绑定这个名字到不同的对象. 比如函数的本地变量对函数内部定义的其他函数和类是可见的, 除非内部的函数又给这个变量赋值. (注意相对于函数代码块, 类代码块使用不同的规则, 后面会介绍)
1 | def test(): |
{% quote %} 可见范围前面的定语只能是名字, 一个名字可以有多个嵌套的可见范围. 每个可见范围里该名字可以绑定到不同的对象. {% endquote %}
有了以上这些概念, 就很容易定义解析的程序了. 当一个代码块中某条语句使用了一个名字, 这个名字将被解析为最包括这个语句的这个名字的最小可视范围里的绑定.
概念: 代码块的环境(environment)
对一个代码块来说, 所有对其可见的可见范围(包含该代码块的含有名字绑定的其他代码块)的集合叫做该代码块的环境.
{% quote %} 代码块的环境就是代码块所有可用的名字. 注意, 由于像if语句, for语句内部的语句不构成代码块. 在其内部绑定的名字的可视范围是包含该if语句或者for语句的代码块. {% endquote %}
当一个名字在所有可见范围内都找不到绑定, 那么对其解析失败.
一个NameError
异常将被引发. 如果在函数中含有一个名字的绑定,
使该名字成为一个本地变量. 但是用该变量的语句在绑定之前,
那么NameError
异常的一个子类UnboundLocalError
异常将被引发.
1 | a |
如果一个名字的绑定操作发生在一个代码块的任何位置, 在该代码块中所有对这个名字的使用都将被视作对当前块的引用 (这句话我理解就是名字被视作当前块的局部变量). 因此在绑定名称之前在块中使用名称时, 这可能导致错误. 这个规则很微妙. Python缺少声明, 并且允许名称绑定操作发生在代码块内的任何位置. 可以通过扫描代码块的整个文本寻找名称绑定操作来确定代码块的局部变量。
概念: 名字空间(namespace).
在一个代码块内绑定的所有名字的集合叫代码块的名字空间.
全局名字空间(global namespace)是在一个包含当前代码块的模块的名字空间.
内建名字空间(builtins namespace),
是指模块builtins
的全局名字空间. 顶级名字空间(top-level
namespace)是指全局名字空间和内建名字空间的合集,
全局名字空间具有更高优先级.
在一个代码块里使用global
语句声明的名字,
将被视为全局变量, 在顶级名字空间中寻找绑定.
global
语句跟同一个代码块中的绑定操作具有相同的可见范围,
所以如果自由变量的最小可视范围内有global
语句,
自由变量将没视作全局变量.
1 | 1 a = |
另一个跟可见范围相关的语句是nonlocal
语句,
用其声明的变量将不使用最近函数可见范围内的绑定, 而是用之前的一次绑定.
如果给定的名字没有在任何函数可见范围绑定,
异常SyntaxError
将被引发, 即使名字在全局名字空间中绑定.
1 | 1 a = |
模块的名字空间在模块第一次被导入(import)的时候自动创建.
一个脚本的主模块总是被叫做__main__
.
名字解析规则的两个特例类定义代码块和作为exec()
或者eval()
字符串参数的代码块.
先说类定义代码块.
一个例外的规则就是在类定义中使用的没有绑定的本地变量,
将直接到全局名字空间中查找,
而不是像前面描述的规则到最接近的外层可见范围(类定义本身的名字空间)查找.
类定义的名字空间将变为类的属性字典. 因此下面的例子里直接在方法里面使用a,
将会引发NameError
.
1 | class A: |
还有一点类定义代码块下的名字的可见范围被限制在类定义代码块本身,
而无法像函数代码块那样向下延伸到类方法的代码块.
这也包含推导式(comprehensions)或者生成器(generator),
因为他们本质上就是用函数可见范围来实现的. 例如下面的代码将执行成功:
1
2
3
4
5
6
7class A:
1 a =
b = a
...
class A:
1 a =
for i in range(10)] b = [A.a + i
但下面的代码将执行失败: 1
2
3
4
5
6
7
8
9class A:
42 a =
list(a + i for i in range(10)) b =
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in A
File "<stdin>", line 3, in <genexpr>
NameError: name 'a' is not defined
4.2.3. 内建名字和受限执行
CPython的执行细节:
Python用户不应该碰__builtins__
对象. 这是一个实现细节.
如果确实想要覆盖内建名字空间中的值,
用户应该导入buitins模块并适当修改其attribute.
事实上, 一个代码块关联的内建名字空间,
是通过在该代码块的全局名字空间里查找名字__builtins__
来实现的.
这个名字空间应该或者是一个字典, 或者是一个模块(后一种情况,
使用模块的字典). 默认情况下, 在__main__
模块里,
__builtins__
是内建模块builtins
; 在其他模块里,
__builtins__
是`builtins模块本身的字典的别名.
file1: main.py 1
2
3
4import othermodule
import builtins
print('{}:{}'.format(__name__, __builtins__ is builtins))
print('{}:{}'.format(__name__, __builtins__ is builtins.__dict__))
file2: othermodule.py 1
2
3import builtins
print('{}:{}'.format(__name__, __builtins__ is builtins))
print('{}:{}'.format(__name__, __builtins__ is builtins.__dict__))
执行main.py可以得到:
1 | othermodule:False |
4.2.4. 与动态特性的交互
自由变量的名字解析在运行时发生, 而不是编译时.
导致eval()
和exec()
函数无权访问用于解析名子的完整环境.
名字仅可能在调用者的局部名字空间或者全局名字空间里解析.
自由变量并不是在最小可视范围内解析, 而是在全局变量里解析.
这两个函数都可以通过可选的参数来覆盖全局和局部名字空间.
如果只设置一个名字空间, 全局和局部都使用该名字空间.
4.3. 异常(Exceptions)
异常是一个打破代码块正常控制流的方法, 以便处理错误或其他特殊情况. 在检测到错误的地方引发了异常; 它可以由周围的代码块来处理, 也可以由直接或间接调用发生错误的代码块的代码块来处理。
Python解释器在检测到运行时错误(例如除以零)时会引发异常.
Python程序还可以使用raise
语句显式引发异常.
异常处理程序通过try...except
语句指定.
语句的finally
子语句可用于指定清理代码.
该子语句不会处理异常, 无论先前代码中是否发生异常,
该清理代码都会执行.
Python使用错误处理的模型是"终结"(termination)模型: 即异常处理程序可以找出发生了什么并继续在外部执行, 但是它无法修复错误并重试失败的操作(除非重新输入有问题的代码的顶部).
当根本没有处理异常时, 解释器终止程序的执行, 或返回其交互式主循环. 在这两种情况下, 除非异常为SystemExit, 否则它将打印堆栈回溯.
异常由类实例标识. 根据实例的类来选择except子语句: except子语句必须引用实例的类或其基类. 该实例可以被异常处理程序接收, 并且可以携带有关异常条件的其他信息。
注意: 异常消息(Exception messages)不是Python API的一部分. 当Python版本更新, 它们的内容可能会在没有警告的情况下改变. 将在多个版本的解释器下运行的代码不应该依赖于异常消息.
专有名词翻译
英文 | 中文 |
---|---|
attributes | 属性(翻译与property无法区分, 直接使用英文) |
builtin | 内建 |
clause | 子语句 |
code block/block | 代码块 |
comprehension | 推导式 |
generator | 生成器 |
identifier | 标识符 |
import | 导入 |
namespace | 名字空间 |
module | 模块 |
model | 模型 |
object | 对象 |
scope | 可见范围 |
top-level | 顶级 |
variable | 变量 |