变量作用域与递归
学习层次
变量作用域、参数传递和返回值属于核心掌握;递归能够读懂执行过程并写出基础终止条件即可,复杂递归作为拓展回查。
函数把代码划分成了相互独立的执行区域。在开发学生管理系统或业务工具时,我们常遇到两个现实问题:
- 为什么在函数外定义的配置,有的函数能读却不能直接改?函数内外同名变量到底谁生效?
- 面对需要层层拆解的数学计算或嵌套结构,如何让函数调用自己来逐步缩小问题规模,又该如何防止程序陷入死循环?
本节围绕“变量的可见边界”与“自我调用的收敛机制”展开学习。
变量作用域
作用域是指变量能够被有效访问的代码区域。当函数内外出现同名变量时,Python 需要一套明确的查找规则来决定读取或修改哪个变量。
在基础编程中,最核心的是两种作用域:
- 全局作用域:定义在所有函数外部的代码区域,生命周期伴随整个程序的执行。
- 局部作用域:定义在某个函数内部的代码区域,生命周期仅在函数单次调用期间存在。
全局变量与局部变量
全局变量的可见性:
定义在函数外部的变量称为全局变量。当前模块内的所有函数默认都可以直接读取它:
system_name = "学生信息管理系统"
def show_title():
print(system_name)
show_title()
print(system_name)运行结果:
学生信息管理系统
学生信息管理系统system_name 定义在全局作用域中,因此函数内外均可直接访问。
常量命名约定:
部分全局变量用于保存固定配置(如系统阈值或数学常数):
PASS_SCORE = 60
PI = 3.1415926Python 语言层面没有专门的常量关键字。变量名全部大写是一种团队开发约定,用以提示“该配置在程序运行期间不应随意修改”。
可变全局状态的隐患:
虽然全局变量可以被任意函数读取,但如果在多个函数中频繁依赖或修改全局变量,会带来明显问题:
- 多个函数都能修改全局变量,状态变化难以追踪;
- 函数对外部环境产生强依赖,脱离特定上下文后无法单独复用和测试;
- 多模块协同或并发执行时容易引发数据污染。
因此,固定配置适合定义为全局变量;业务数据应优先通过形参传入、通过返回值传出。
局部变量与作用域隔离:
定义在函数内部的变量(包括函数形参)称为局部变量。局部变量只在所属函数内部有效:
number = 100
def get_number():
number = 15
print("函数内部:", number)
return number
result = get_number()
print("函数返回值:", result)
print("函数外部:", number)运行结果:
函数内部: 15
函数返回值: 15
函数外部: 100尽管函数内外都使用了变量名 number,但它们属于不同作用域下的两个独立对象。在函数内部访问 number 时,Python 优先查找局部作用域(就近原则),因此函数内部打印的是 15,而全局变量 number 保持 100 不变。
局部变量的生命周期:
函数调用开始时创建局部作用域,函数调用结束并返回后局部变量被销毁。外部代码无法直接访问函数内的局部变量:
def create_name():
student_name = "张三"
return student_name
create_name()
print(student_name) # NameError: name 'student_name' is not defined若外部需要使用函数计算产出的数据,必须通过 return 将其作为返回值传出,并在调用处使用变量接收。
变量遮蔽与 UnboundLocalError
如果在函数内部单纯读取全局变量是可行的,那么在函数内直接修改它会发生什么?先观察下面的计数器累加示例:
count = 10
def increase_count():
count += 1
increase_count()运行程序会立即抛出异常:
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 关键字声明:
count = 10
def increase_count():
global count
count += 1
print("函数内部:", count)
print("调用前:", count)
increase_count()
print("调用后:", count)运行结果:
调用前: 10
函数内部: 11
调用后: 11global count 明确指示解释器:当前函数内出现的 count 是全局命名空间中的全局变量,而不是新建的局部变量。
显式数据传递优先原则:
global 虽然能解决修改问题,但在实际项目中应尽量克制使用。更好的实践是通过参数输入与返回值输出显式传递数据:
def increase_count(count):
return count + 1
count = 10
count = increase_count(count)
print(count)通过“参数传入状态、返回值更新状态”,函数的输入与输出边界清晰透明,既消除了隐式副作用,又便于单元测试。
递归机制
递归是指一个函数在函数体内直接或间接调用自身。
面对层层嵌套或自相似结构的大问题,直接用循环编写有时逻辑较为繁琐;递归的核心思路是:将大问题拆解为同结构的子问题,层层递进直到最简单的基准情况,然后逐层回溯返回结果。
先通过一个最小的倒计时函数观察递归的执行节奏:
def countdown(number):
print(number)
if number == 0:
return
countdown(number - 1)
countdown(3)运行结果:
3
2
1
0执行流程拆解:
- 调用
countdown(3),打印3,检测未满足停止条件,递归调用countdown(2); - 进入新的一层
countdown(2),打印2,继续递归调用countdown(1); - 进入
countdown(1),打印1,调用countdown(0); - 进入
countdown(0),打印0,命中number == 0,执行return结束本层; - 上层函数依次结束并退栈,整个流程完毕。
终止条件与递推步骤
编写任何递归函数,必须严格包含两个核心组成要素:
- 基准终止条件(Base Case):什么条件下停止递归直接返回结果,不再继续向下调用自身。
- 递推缩小步骤(Recursive Step):如何将当前问题缩减为规模更小的子问题,并再次调用自身。
def 递归函数(参数):
if 满足基准终止条件:
return 明确的基准值
return 递推表达式(缩小规模的参数)如果遗漏了终止条件,或者递推步骤没有让参数逐步逼近终止条件,函数就会无休止地创建新层级。当调用深度超出系统上限时,Python 会直接报错:
RecursionError: maximum recursion depth exceeded案例:阶乘计算
需求:使用递归函数计算指定非负整数
功能分析:
- 数学定义:
;当 时, 。 - 基准终止条件:当
时,直接返回 。 - 递推步骤:返回
,参数每次减 ,逐步向 收敛。
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5))运行结果:
120递归调用栈展开与回溯过程:
以 factorial(5) 为例,执行过程分为“递推压栈”与“回溯计算”两个阶段:
递推压栈阶段(问题逐级下压):
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案例:调和数累加
需求:使用递归函数计算第
功能分析:
- 第
阶调和数的数学公式为: - 递推拆解关系:
- 基准终止条件:
(当 时返回 ); - 递推步骤:当
时, 。
- 基准终止条件:
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}")运行结果:
H1 = 1.0000
H2 = 1.5000
H3 = 1.8333
H4 = 2.0833
H5 = 2.2833该实现要求参数 harmonic(n - 1) 缩小计算规模,最终在
递归边界与栈深度风险
递归虽然在结构上高度契合某些数学定义与树状结构,但在工程实践中并非处处适用。
每次执行函数调用,Python 都会在内存中创建一个栈帧(Stack Frame),用于保存该层函数的局部变量、实参与返回地址。在递推触底之前,所有未完成的栈帧都会持续驻留在调用栈中。
使用递归时必须进行三重边界检查:
- 是否存在确切的终止条件:避免因逻辑漏洞进入无限递归;
- 递推步长是否向终止条件有效收敛:避免参数发散跳过终止判断;
- 调用规模是否触及最大栈深度:Python 默认的最大递归深度通常约为 1000 层,超出即抛出
RecursionError。
技术选型建议:
对于平铺的线性累加、计数或遍历问题,优先选用 for 或 while 循环,执行效率更高且内存恒定;只有面对树形目录遍历、分治排序或嵌套结构解析等天然递归场景时,才优先选用递归算法。
小结
本节围绕五个问题掌握变量作用域与递归机制:
- 同名变量读取顺序:优先使用当前函数内部的局部变量,若局部不存在才向外层全局作用域查找;
- 局部变量的生存期:局部变量随函数调用创建,随函数返回销毁;外部必须通过返回值接收结果;
- UnboundLocalError 的原因:函数内若存在对某变量的赋值,该变量即被静态视为局部变量;若在此之前尝试读取它,就会触发未绑定报错;
- 状态修改的最佳实践:修改全局变量可用
global,但更推荐“参数传入 + 返回值传出”的显式数据流设计; - 递归成立的双要素:必须具备明确的基准终止条件(Base Case)与收敛的递推缩小步骤(Recursive Step),谨防栈溢出。
练习
- 编写代码验证局部变量的独立性:在全局定义变量
score = 80,在函数内修改为score = 95,分别打印函数内外score的值; - 编写计数器更新逻辑,分别使用
global与“形参接收 + 返回值更新”两种方案实现计数累加,并对比两者的代码耦合度; - 编写递归函数
sum_numbers(n),计算从累加到 的和( ),并指出其中的基准条件与递推步骤; - 修改
countdown()案例代码,使参数从指定的正整数倒数至后打印 "火箭发射!",并测试传入或负数时的防护逻辑。