Skip to content

变量作用域与递归 ​

学习层次

变量作用域、参数传递和返回值属于核心掌握;递归能够读懂执行过程并写出基础终止条件即可,复杂递归作为拓展回查。

函数把代码划分成了相互独立的执行区域。在开发学生管理系统或业务工具时,我们常遇到两个现实问题:

  1. 为什么在函数外定义的配置,有的函数能读却不能直接改?函数内外同名变量到底谁生效?
  2. 面对需要层层拆解的数学计算或嵌套结构,如何让函数调用自己来逐步缩小问题规模,又该如何防止程序陷入死循环?

本节围绕“变量的可见边界”与“自我调用的收敛机制”展开学习。

变量作用域 ​

作用域是指变量能够被有效访问的代码区域。当函数内外出现同名变量时,Python 需要一套明确的查找规则来决定读取或修改哪个变量。

在基础编程中,最核心的是两种作用域:

  • 全局作用域:定义在所有函数外部的代码区域,生命周期伴随整个程序的执行。
  • 局部作用域:定义在某个函数内部的代码区域,生命周期仅在函数单次调用期间存在。

全局变量与局部变量 ​

全局变量的可见性:

定义在函数外部的变量称为全局变量。当前模块内的所有函数默认都可以直接读取它:

python
system_name = "学生信息管理系统"


def show_title():
    print(system_name)


show_title()
print(system_name)

运行结果:

text
学生信息管理系统
学生信息管理系统

system_name 定义在全局作用域中,因此函数内外均可直接访问。

常量命名约定:

部分全局变量用于保存固定配置(如系统阈值或数学常数):

python
PASS_SCORE = 60
PI = 3.1415926

Python 语言层面没有专门的常量关键字。变量名全部大写是一种团队开发约定,用以提示“该配置在程序运行期间不应随意修改”。

可变全局状态的隐患:

虽然全局变量可以被任意函数读取,但如果在多个函数中频繁依赖或修改全局变量,会带来明显问题:

  1. 多个函数都能修改全局变量,状态变化难以追踪;
  2. 函数对外部环境产生强依赖,脱离特定上下文后无法单独复用和测试;
  3. 多模块协同或并发执行时容易引发数据污染。

因此,固定配置适合定义为全局变量;业务数据应优先通过形参传入、通过返回值传出。

局部变量与作用域隔离:

定义在函数内部的变量(包括函数形参)称为局部变量。局部变量只在所属函数内部有效:

python
number = 100


def get_number():
    number = 15
    print("函数内部:", number)
    return number


result = get_number()
print("函数返回值:", result)
print("函数外部:", number)

运行结果:

text
函数内部: 15
函数返回值: 15
函数外部: 100

尽管函数内外都使用了变量名 number,但它们属于不同作用域下的两个独立对象。在函数内部访问 number 时,Python 优先查找局部作用域(就近原则),因此函数内部打印的是 15,而全局变量 number 保持 100 不变。

全局作用域与局部作用域的边界

局部变量的生命周期:

函数调用开始时创建局部作用域,函数调用结束并返回后局部变量被销毁。外部代码无法直接访问函数内的局部变量:

python
def create_name():
    student_name = "张三"
    return student_name


create_name()
print(student_name)  # NameError: name 'student_name' is not defined

若外部需要使用函数计算产出的数据,必须通过 return 将其作为返回值传出,并在调用处使用变量接收。

变量遮蔽与 UnboundLocalError ​

如果在函数内部单纯读取全局变量是可行的,那么在函数内直接修改它会发生什么?先观察下面的计数器累加示例:

python
count = 10


def increase_count():
    count += 1


increase_count()

运行程序会立即抛出异常:

text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

报错成因剖析:

count += 1 相当于 count = count + 1。Python 解释器在编译当前函数时,发现函数体内存在给变量 count 赋值的操作,因此静态地将 count 判定为局部变量。

当代码运行到 count + 1 时,解释器尝试读取局部变量 count 的值;但在此刻该局部变量尚未完成赋值绑定,因此抛出“未绑定局部变量”错误(UnboundLocalError)。

状态修改与 global 关键字 ​

若确实需要在函数内部修改全局变量的值,必须在函数首行使用 global 关键字声明:

python
count = 10


def increase_count():
    global count
    count += 1
    print("函数内部:", count)


print("调用前:", count)
increase_count()
print("调用后:", count)

运行结果:

text
调用前: 10
函数内部: 11
调用后: 11

global count 明确指示解释器:当前函数内出现的 count 是全局命名空间中的全局变量,而不是新建的局部变量。

显式数据传递优先原则:

global 虽然能解决修改问题,但在实际项目中应尽量克制使用。更好的实践是通过参数输入与返回值输出显式传递数据:

python
def increase_count(count):
    return count + 1


count = 10
count = increase_count(count)
print(count)

通过“参数传入状态、返回值更新状态”,函数的输入与输出边界清晰透明,既消除了隐式副作用,又便于单元测试。

global 与参数返回值的数据流对比

递归机制 ​

递归是指一个函数在函数体内直接或间接调用自身。

面对层层嵌套或自相似结构的大问题,直接用循环编写有时逻辑较为繁琐;递归的核心思路是:将大问题拆解为同结构的子问题,层层递进直到最简单的基准情况,然后逐层回溯返回结果。

先通过一个最小的倒计时函数观察递归的执行节奏:

python
def countdown(number):
    print(number)

    if number == 0:
        return

    countdown(number - 1)


countdown(3)

运行结果:

text
3
2
1
0

执行流程拆解:

  1. 调用 countdown(3),打印 3,检测未满足停止条件,递归调用 countdown(2);
  2. 进入新的一层 countdown(2),打印 2,继续递归调用 countdown(1);
  3. 进入 countdown(1),打印 1,调用 countdown(0);
  4. 进入 countdown(0),打印 0,命中 number == 0,执行 return 结束本层;
  5. 上层函数依次结束并退栈,整个流程完毕。

递归函数的缩小与终止

终止条件与递推步骤 ​

编写任何递归函数,必须严格包含两个核心组成要素:

  1. 基准终止条件(Base Case):什么条件下停止递归直接返回结果,不再继续向下调用自身。
  2. 递推缩小步骤(Recursive Step):如何将当前问题缩减为规模更小的子问题,并再次调用自身。
text
def 递归函数(参数):
    if 满足基准终止条件:
        return 明确的基准值
    
    return 递推表达式(缩小规模的参数)

如果遗漏了终止条件,或者递推步骤没有让参数逐步逼近终止条件,函数就会无休止地创建新层级。当调用深度超出系统上限时,Python 会直接报错:

text
RecursionError: maximum recursion depth exceeded

案例:阶乘计算 ​

需求:使用递归函数计算指定非负整数 n 的阶乘(即 n!)。

功能分析:

  • 数学定义:0!=1;当 n>0 时,n!=n×(n−1)!。
  • 基准终止条件:当 n==0 时,直接返回 1。
  • 递推步骤:返回 n×factorial(n−1),参数每次减 1,逐步向 0 收敛。
python
def factorial(n):
    if n == 0:
        return 1

    return n * factorial(n - 1)


print(factorial(5))

运行结果:

text
120

递归调用栈展开与回溯过程:

以 factorial(5) 为例,执行过程分为“递推压栈”与“回溯计算”两个阶段:

text
递推压栈阶段(问题逐级下压):
factorial(5)
= 5 * factorial(4)
= 5 * (4 * factorial(3))
= 5 * (4 * (3 * factorial(2)))
= 5 * (4 * (3 * (2 * factorial(1))))
= 5 * (4 * (3 * (2 * (1 * factorial(0)))))

触底命中基准条件:
factorial(0) 返回 1

回溯计算阶段(结果逐级向上弹栈):
= 5 * (4 * (3 * (2 * (1 * 1))))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120

阶乘递归调用栈与逐层返回

案例:调和数累加 ​

需求:使用递归函数计算第 n 阶调和数 Hn。

功能分析:

  • 第 n 阶调和数的数学公式为:Hn=1+12+13+⋯+1n
  • 递推拆解关系:
    • 基准终止条件:H1=1(当 n==1 时返回 1);
    • 递推步骤:当 n>1 时,Hn=Hn−1+1n。
python
def harmonic(n):
    if n == 1:
        return 1

    return harmonic(n - 1) + 1 / n


for number in range(1, 6):
    print(f"H{number} = {harmonic(number):.4f}")

运行结果:

text
H1 = 1.0000
H2 = 1.5000
H3 = 1.8333
H4 = 2.0833
H5 = 2.2833

该实现要求参数 n 为正整数。每次递推调用 harmonic(n - 1) 缩小计算规模,最终在 n==1 处返回基准值并逐步完成浮点累加。

递归边界与栈深度风险 ​

递归虽然在结构上高度契合某些数学定义与树状结构,但在工程实践中并非处处适用。

每次执行函数调用,Python 都会在内存中创建一个栈帧(Stack Frame),用于保存该层函数的局部变量、实参与返回地址。在递推触底之前,所有未完成的栈帧都会持续驻留在调用栈中。

使用递归时必须进行三重边界检查:

  1. 是否存在确切的终止条件:避免因逻辑漏洞进入无限递归;
  2. 递推步长是否向终止条件有效收敛:避免参数发散跳过终止判断;
  3. 调用规模是否触及最大栈深度:Python 默认的最大递归深度通常约为 1000 层,超出即抛出 RecursionError。

技术选型建议:

对于平铺的线性累加、计数或遍历问题,优先选用 for 或 while 循环,执行效率更高且内存恒定;只有面对树形目录遍历、分治排序或嵌套结构解析等天然递归场景时,才优先选用递归算法。

小结 ​

本节围绕五个问题掌握变量作用域与递归机制:

  1. 同名变量读取顺序:优先使用当前函数内部的局部变量,若局部不存在才向外层全局作用域查找;
  2. 局部变量的生存期:局部变量随函数调用创建,随函数返回销毁;外部必须通过返回值接收结果;
  3. UnboundLocalError 的原因:函数内若存在对某变量的赋值,该变量即被静态视为局部变量;若在此之前尝试读取它,就会触发未绑定报错;
  4. 状态修改的最佳实践:修改全局变量可用 global,但更推荐“参数传入 + 返回值传出”的显式数据流设计;
  5. 递归成立的双要素:必须具备明确的基准终止条件(Base Case)与收敛的递推缩小步骤(Recursive Step),谨防栈溢出。

练习 ​

  1. 编写代码验证局部变量的独立性:在全局定义变量 score = 80,在函数内修改为 score = 95,分别打印函数内外 score 的值;
  2. 编写计数器更新逻辑,分别使用 global 与“形参接收 + 返回值更新”两种方案实现计数累加,并对比两者的代码耦合度;
  3. 编写递归函数 sum_numbers(n),计算从 1 累加到 n 的和(1+2+⋯+n),并指出其中的基准条件与递推步骤;
  4. 修改 countdown() 案例代码,使参数从指定的正整数倒数至 1 后打印 "火箭发射!",并测试传入 0 或负数时的防护逻辑。