Python局部变量编译原理
Python local variable compile principle
def fun():
if False:
x=3
print(locals())
print(x)
fun()
输出和错误信息:
{}
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-57-d9deb3063ae1> in <module>()
4 print(locals())
5 print(x)
----> 6 fun()
<ipython-input-57-d9deb3063ae1> in fun()
3 x=3
4 print(locals())
----> 5 print(x)
6 fun()
UnboundLocalError: local variable 'x' referenced before assignment
我想知道 python 解释器是如何工作的。请注意,x=3 根本不是 运行,它不应被视为局部变量,这意味着错误将是“名称 'x' 未定义”。但是查看代码和错误消息,情况并非如此。谁能解释一下这种情况背后python解释器的编译机制原理?
x = 3
无法访问这一事实无关紧要。函数分配给它,所以它必须是本地名称。
请记住,整个文件是在执行开始之前编译的,但是函数是在执行阶段定义的,当编译的函数定义块被执行时,创建函数对象。
一个复杂的优化器可以消除无法访问的代码,但是 CPython 的优化器并不那么聪明——它只执行非常简单的锁孔优化。
要更深入地了解 Python 内部结构,请查看 ast 和 dis 模块。
函数中使用的名称对于整个函数体只能有一个作用域。范围是在编译时确定的(而不是在函数为 运行 时)。
如果函数中的任何地方都有一个名称赋值(无论调用函数时它是否为 运行),编译器默认将该名称视为函数的局部名称。您可以使用 global
和 nonlocal
语句明确告诉它使用不同的范围。
一种特殊情况是在一个函数的主体中分配了一个名称,并从第一个函数中定义的另一个函数访问。这样的变量将放在一个特殊的 closure
单元格中,该单元格将在函数之间共享。外部函数会将变量视为局部变量,而内部函数只有在名称有 nonlocal
语句时才能分配给它。这是闭包和 nonlocal
语句的示例:
def counter():
val = 0
def helper():
nonlocal val
val += 1
return val
return helper
除了您看到的问题之外,您可能还会看到另一种范围混淆:
x = 1
def foo():
print(x) # you might expect this to print the global x, but it raises an exception
x = 2 # this assignment makes the compiler treat the name x as local to the function
在 foo
函数中,名称 x
在任何地方都被认为是本地的,即使 print
调用在它被分配到本地名称空间之前尝试使用它。
因此,Python 将始终将每个函数中的每个名称归类为 local、non-local 或全球。这些名称范围是排他的;在每个函数中(嵌套函数中的名称都有自己的命名范围),每个名称只能属于这些类别之一。
当Python编译这段代码时:
def fun():
if False:
x=3
它将产生一个抽象语法树,如下所示:
FunctionDef(
name='fun',
args=arguments(...), b
body=[
If(test=NameConstant(value=False),
body=[
Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=3))
],
orelse=[])
]
)
(为简洁起见省略了一些内容)。现在,当这个抽象语法树被编译成代码时,Python 将扫描所有名称节点。如果有任何 Name
节点,具有 ctx=Store()
,则该名称被认为是 local 到封闭的 FunctionDef
(如果有),除非被 global
(即 global x
)或 nonlocal
(nonlocal x
)相同函数定义中的语句。
ctx=Store()
主要出现在相关名称用于赋值语句的左侧,或用作 for
循环中的迭代变量时。
现在,当Python将其编译成字节码时,生成的字节码是
>>> dis.dis(fun)
4 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
优化器完全删除了 if
语句;然而,由于变量已经标记为函数的局部变量,LOAD_FAST
用于 x
,这将导致 x
从 local 变量访问, 和局部变量。由于尚未设置 x
,因此抛出 UnboundLocalError
。另一方面,名称 print
从未分配给,因此被视为此函数中的全局名称,因此其值加载为 LOAD_GLOBAL
.
def fun():
if False:
x=3
print(locals())
print(x)
fun()
输出和错误信息:
{}
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-57-d9deb3063ae1> in <module>()
4 print(locals())
5 print(x)
----> 6 fun()
<ipython-input-57-d9deb3063ae1> in fun()
3 x=3
4 print(locals())
----> 5 print(x)
6 fun()
UnboundLocalError: local variable 'x' referenced before assignment
我想知道 python 解释器是如何工作的。请注意,x=3 根本不是 运行,它不应被视为局部变量,这意味着错误将是“名称 'x' 未定义”。但是查看代码和错误消息,情况并非如此。谁能解释一下这种情况背后python解释器的编译机制原理?
x = 3
无法访问这一事实无关紧要。函数分配给它,所以它必须是本地名称。
请记住,整个文件是在执行开始之前编译的,但是函数是在执行阶段定义的,当编译的函数定义块被执行时,创建函数对象。
一个复杂的优化器可以消除无法访问的代码,但是 CPython 的优化器并不那么聪明——它只执行非常简单的锁孔优化。
要更深入地了解 Python 内部结构,请查看 ast 和 dis 模块。
函数中使用的名称对于整个函数体只能有一个作用域。范围是在编译时确定的(而不是在函数为 运行 时)。
如果函数中的任何地方都有一个名称赋值(无论调用函数时它是否为 运行),编译器默认将该名称视为函数的局部名称。您可以使用 global
和 nonlocal
语句明确告诉它使用不同的范围。
一种特殊情况是在一个函数的主体中分配了一个名称,并从第一个函数中定义的另一个函数访问。这样的变量将放在一个特殊的 closure
单元格中,该单元格将在函数之间共享。外部函数会将变量视为局部变量,而内部函数只有在名称有 nonlocal
语句时才能分配给它。这是闭包和 nonlocal
语句的示例:
def counter():
val = 0
def helper():
nonlocal val
val += 1
return val
return helper
除了您看到的问题之外,您可能还会看到另一种范围混淆:
x = 1
def foo():
print(x) # you might expect this to print the global x, but it raises an exception
x = 2 # this assignment makes the compiler treat the name x as local to the function
在 foo
函数中,名称 x
在任何地方都被认为是本地的,即使 print
调用在它被分配到本地名称空间之前尝试使用它。
因此,Python 将始终将每个函数中的每个名称归类为 local、non-local 或全球。这些名称范围是排他的;在每个函数中(嵌套函数中的名称都有自己的命名范围),每个名称只能属于这些类别之一。
当Python编译这段代码时:
def fun():
if False:
x=3
它将产生一个抽象语法树,如下所示:
FunctionDef(
name='fun',
args=arguments(...), b
body=[
If(test=NameConstant(value=False),
body=[
Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=3))
],
orelse=[])
]
)
(为简洁起见省略了一些内容)。现在,当这个抽象语法树被编译成代码时,Python 将扫描所有名称节点。如果有任何 Name
节点,具有 ctx=Store()
,则该名称被认为是 local 到封闭的 FunctionDef
(如果有),除非被 global
(即 global x
)或 nonlocal
(nonlocal x
)相同函数定义中的语句。
ctx=Store()
主要出现在相关名称用于赋值语句的左侧,或用作 for
循环中的迭代变量时。
现在,当Python将其编译成字节码时,生成的字节码是
>>> dis.dis(fun)
4 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
优化器完全删除了 if
语句;然而,由于变量已经标记为函数的局部变量,LOAD_FAST
用于 x
,这将导致 x
从 local 变量访问, 和局部变量。由于尚未设置 x
,因此抛出 UnboundLocalError
。另一方面,名称 print
从未分配给,因此被视为此函数中的全局名称,因此其值加载为 LOAD_GLOBAL
.