一、引言

当你在 Python 中调用一个函数时,解释器在背后做了大量工作来管理变量的"可见性"。为什么函数内部能访问全局变量,但全局却不能直接看到函数内部的变量?为什么嵌套函数能记住外层函数的变量?这一切的答案,都指向一个核心概念——环境模型(Environment Model)

本文源自 UC Berkeley CS61A 课程的核心内容,带你从"框架(Frame)"的视角理解 Python 的函数调用机制。

二、什么是环境模型

环境模型是 Python 解释器用来追踪变量名与值之间绑定关系的一套机制。它的核心思想极其简单:

一个表达式在特定环境中被求值。环境由一系列框架(Frame)组成,每个框架包含一组绑定(Binding)——即变量名到值的映射。

在这套模型中,有两种最关键的结构:

  • 全局框架(Global Frame):程序启动时就存在的唯一框架,存储全局变量和函数定义
  • 局部框架(Local Frame):每次函数调用时动态创建的新框架,存储函数的形参和局部变量

三、框架是什么

框架本质上是一个上下文(Context),记录着"在这个范围内,哪些名字指向哪些值"。你可以把它想象成一张表格:

名字(Name) 值(Value)
x 10
square func square(x) {...}

当你引用一个名字时,Python 从当前框架开始查找;如果找不到,就顺着"父框架"的指针向外查找,直到全局框架。如果在全局框架也找不到,就会抛出 NameError

四、外框架与内框架

这是理解环境模型的关键区分:

外框架(Outer Frame / Enclosing Frame)

外框架是函数定义时所在的那个环境。换句话说,函数"记住"了自己是在哪里被定义的,后续所有变量查找都会从那里开始向外延伸。

1
2
3
4
5
6
7
8
9
10
11
x = 10          # 全局框架中 x = 10

def outer():
x = 20 # outer 的局部框架中 x = 20

def inner():
print(x) # inner 中引用 x,该去哪找?
return inner

f = outer()
f() # 输出:20

这里 inner 是在 outer 的内部定义的,所以 inner外框架就是 outer 的局部框架。当 inner 中引用 x 时:

  1. 先在 inner 自己的局部框架中找 —— 没有
  2. 再去外框架(outer 的局部框架)找 —— 找到 x = 20

所以输出是 20,而不是全局的 10。这就是**词法作用域(Lexical Scope)**的核心规则:查找路径由函数定义的位置决定,而不是调用的位置。

内框架(Inner Frame / Local Frame)

内框架就是当前函数调用创建的新框架。每次调用函数,哪怕是同一个函数,都会创建一个全新的局部框架:

1
2
3
4
5
6
7
8
9
10
def add_n(n):
def inner(x):
return x + n
return inner

add_3 = add_n(3) # 第一次调用,创建一个局部框架,n=3
add_5 = add_n(5) # 第二次调用,创建一个全新的局部框架,n=5

print(add_3(10)) # 输出:13
print(add_5(10)) # 输出:15

add_n(3) 调用时,创建了一个局部框架,里面 n = 3。这个框架被返回的 inner 函数"记住"了。add_n(5) 再次调用时,创建了另一个完全独立的局部框架,里面 n = 5。两次调用产生了两个互不干扰的"记忆"——这就是闭包的本质。

五、环境模型的查找规则

整个环境是一个框架链表。每次查找变量时,Python 遵循这样的路径:

1
当前局部框架 → 外框架(定义时环境) → 更外层框架 → ... → 全局框架

这解释了下面这段代码的行为:

1
2
3
4
5
6
7
8
9
10
11
x = "global"

def f1():
x = "f1"

def f2():
x = "f2"
print(x) # 在 f2 自己的框架中就找到了
f2()

f1() # 输出:f2

以及:

1
2
3
4
5
6
7
8
9
10
x = "global"

def f1():
x = "f1"

def f2():
print(x) # f2 中没有,去外框架(f1)中找
f2()

f1() # 输出:f1

六、可视化:从环境图理解嵌套函数

1
2
3
4
5
6
7
8
9
10
11
x = 1

def outer(y):
z = 10
def inner():
return x + y + z
return inner

f = outer(5)
result = f()
print(result) # 输出:16

这段代码的环境图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
全局框架:
x → 1
outer → func outer(y) {...}
f → func inner() {...} [外框架 → outer调用框架]

outer(5) 调用框架:
y → 5
z → 10
inner → func inner() {...} [外框架 → outer调用框架]
返回值 → inner 函数对象

inner() 调用框架:
(空,无局部变量)
查找 x:当前框架无 → outer调用框架无 → 全局框架,x=1
查找 y:当前框架无 → outer调用框架,y=5
查找 z:当前框架无 → outer调用框架,z=10
返回值:1+5+10=16

注意关键点:inner 函数对象保存了一个指向 outer(5) 调用框架 的引用——这就是它的外框架。即使 outer(5) 已经执行完毕,这个框架依然不会被销毁,因为 inner 还"抓着"它不放。

七、与 C++ 的对比

概念 Python(环境模型) C++
变量查找 框架链,从内向外 块作用域,从内向外
函数内访问外部变量 通过外框架引用 通过捕获列表(lambda)或直接可见
闭包实现 函数对象持有外框架引用 lambda 捕获列表拷贝/引用
全局变量 global 关键字声明后赋值 直接可见,用 :: 区分
生命期管理 GC,有引用就不销毁 栈上自动销毁,堆上手动管理

C++ 的 lambda 需要显式声明捕获列表([=][&]),而 Python 的闭包自动持有整个外框架——这带来了灵活性,但也意味着需要注意闭包变量的生命期。

八、常见陷阱:循环中的闭包

这是环境模型最经典的陷阱:

1
2
3
4
5
6
funcs = []
for i in range(3):
funcs.append(lambda: i)

for f in funcs:
print(f()) # 输出:2, 2, 2 —— 而不是 0, 1, 2

原因:lambda 的外框架就是全局框架(或包含 for 循环的函数框架),它引用的是变量名 i,而不是变量值。当 lambda 最终被调用时,i 已经变成了 2

修复方式——利用默认参数在定义时"快照"值:

1
2
3
4
5
6
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # i=i 在定义时就把值固定了

for f in funcs:
print(f()) # 输出:0, 1, 2 ✓

九、总结

环境模型是 Python 函数作用域的底层逻辑,掌握它意味着你真正理解了三件事:

  1. 外框架是函数"出生"的地方,决定了它能看见哪些变量。这是词法作用域的核心。
  2. 内框架是函数"执行"的地方,每次调用都全新创建,互不干扰。
  3. 环境是一串框架的链条,变量查找沿着内→外的方向逐级回溯,直到全局。

理解了框架与外框架的关系,闭包不再神秘,嵌套函数不再困惑,nonlocalglobal 的语义也变得理所当然。环境模型不是黑魔法,而是精心设计的、一致的名字查找规则。