在Python
中,变量的使用看起来非常简单,例如 a = 10
,s = "hello"
等等。
然而,这种简单的赋值操作背后,CPython
其实做了很多复杂的工作。
本文将通过一些简单易懂的代码示例,一起探索Python
变量背后的奥秘,让我们对它的实现机制有更深一步的理解。
1. 变量到底是什么?
在Python
中,变量本质上是一个名字到值的映射。
例如,当你写a = 1
时,a
是一个名字,而1
是一个值。
CPython
会将这个名字 和值 关联起来,以便你后续可以通过名字 访问这个值。
python
a = 1
print(a) # 输出:1
这种映射关系是通过一个名为命名空间的结构实现的。
命名空间是一个字典,其中的键是变量名,值是变量对应的对象。
它的定义可参考CPython源码中的Include/internal/pycore_frame.h
文件。
c
typedef struct _PyInterpreterFrame {
// 省略... ...
PyObject *f_globals; /* Borrowed reference. Only valid if not on C stack */
PyObject *f_builtins; /* Borrowed reference. Only valid if not on C stack */
PyObject *f_locals; /* Strong reference, may be NULL. Only valid if not on C stack */
// 省略... ...
}
其中,f_locals
保存局部变量映射,函数执行时,局部变量值存于此;
f_globals
用于全局变量,模块级代码块执行时,f_globals
指向模块全局命名空间字典;
f_builtins
关联内置命名空间。
2. 变量的底层实现:字节码
CPython在执行代码时,会先将代码编译成字节码,然后由虚拟机执行这些字节码。我们可以通过 dis 模块查看代码的字节码。
例如,对于a = 1
,字节码如下:
python
import dis
code = """
a = b
"""
dis.dis(code)

LOAD_NAME
:从命名空间中加载变量b
的值STORE_NAME
:将值存储到变量a
中
这两个指令展示了CPython
如何处理变量的读取和赋值。
3. 命名空间与作用域
Python
中的变量存储在不同的命名空间中,而这些命名空间又与代码的作用域相关,作用域决定了变量的可见性。
Python
有三种主要的作用域:
- 局部作用域:函数内部的变量
- 全局作用域:模块级别的变量
- 内置作用域:包含内置函数和类型的命名空间
python
x = "global" # 全局变量
def func():
y = "local" # 局部变量
print(x) # 输出:global
print(y) # 输出:local
func()

在这个例子中,x
是全局变量,y
是局部变量。
如果在函数中尝试访问一个未定义 的变量,CPython
会按照以下顺序查找:
-
局部命名空间(
f_locals
) -
全局命名空间(
f_globals
) -
内置命名空间(
f_builtins
)
如果仍然找不到,就会抛出NameError
异常。
4. 不同变量的字节码
CPython
为不同作用域的变量提供了不同的字节码指令,以优化性能和实现特定的行为。
4.1. 局部变量
在函数中,局部变量使用LOAD_FAST
和STORE_FAST
指令。
这些指令直接操作一个数组,而不是字典,因此速度更快。
python
def func():
a = 1 # STORE_FAST
b = a # LOAD_FAST
return b
dis.dis(func)

4.2. 全局变量
全局变量使用LOAD_GLOBAL
和STORE_GLOBAL
指令。
这些指令会直接操作全局命名空间。
python
x = 1
def func():
global x
x = 2 # STORE_GLOBAL
return x # LOAD_GLOBAL
dis.dis(func)

4.3. 闭包变量
当函数嵌套时,内部函数可以访问外部函数的变量。
这些变量称为闭包变量 ,使用LOAD_DEREF
和STORE_DEREF
指令。
python
def outer():
x = 1
def inner():
return x # LOAD_DEREF
return inner
dis.dis(outer)

5. 类中的变量
在类定义中,变量的行为与函数不同。
类定义中的变量使用LOAD_NAME
和STORE_NAME
指令,因为类的命名空间会动态地与全局命名空间交互。
python
x = "global"
class MyClass:
print(x) # 使用 LOAD_NAME
x = "local"
print(x) # 使用 LOAD_NAME
MyClass()
输出:

查看指令的话,可以使用:python.exe -m dis .\cpython-variable.py
命令。
如果在类中使用嵌套函数,CPython
会使用LOAD_CLASSDEREF
指令来处理闭包变量。
python
class MyClass:
x = "cell"
def method(self):
print(x) # 使用 LOAD_CLASSDEREF
MyClass().method()
6. 编译器如何选择指令
CPython
的编译器会根据变量的作用域和代码块类型选择合适的字节码指令。
例如:
- 如果变量是局部变量,编译器会生成
LOAD_FAST
和STORE_FAST
- 如果变量是全局变量,编译器会生成
LOAD_GLOBAL
和STORE_GLOBAL
- 如果变量是闭包变量,编译器会生成
LOAD_DEREF
和STORE_DEREF
7. 总结
Python
变量的实现机制比看起来复杂得多,它涉及到字节码指令、命名空间、作用域以及编译器的决策逻辑。
通过理解这些概念,可以更好地掌握Python
的变量行为,尤其是在复杂的作用域场景中。
如果对CPython
的实现感兴趣,可以进一步阅读其源码中与变量相关的部分。