函数使用(二)
前言
- 这一篇讲两件事:函数定义的几条规矩,以及变量作用域(全局 / 局部 /
global/nonlocal)和递归。 - 每个小节后面都配了一个把该节知识点全用上一遍的完整示例,输出是本机 Python 3.13.9 实跑的结果。
- 作用域这块最容易踩的是「函数里先读后写同名变量」和「改全局忘了
global」,示例里都专门演示了报错长什么样。
函数回顾
-
函数的定义位置是随意的吗?
- 不是。Python 自上而下执行,调用发生时函数必须已经定义好,否则会
NameError。 - 但这约束的是「调用」而不是「书写顺序」:函数体里引用后面才定义的函数完全没问题,因为函数体是在被调用时才执行的(递归更是自己引用自己)。
- 不是。Python 自上而下执行,调用发生时函数必须已经定义好,否则会
-
函数的名字可以相同吗?
- 不能靠同名做「重载」:Python 没有 C++/Java 那种按参数类型、个数区分的函数重载,同名定义就是后一个覆盖前一个(函数不过是绑定在这个名字上的对象)。
- 要按参数分支就用默认参数、
*args/**kwargs,或者标准库的functools.singledispatch(按第一个参数的类型分派)。
-
函数中能否再定义函数?
- 能。函数内部定义的函数叫内部函数,它是闭包和装饰器的基础;内层函数引用外层函数的变量时,用
nonlocal才能改那个变量(见下一节)。
- 能。函数内部定义的函数叫内部函数,它是闭包和装饰器的基础;内层函数引用外层函数的变量时,用
-
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44from functools import singledispatch
# 1) 先定义后调用:只要「调用发生时」函数已经存在就行,
# 函数体内引用后面才定义的函数完全没问题
def a():
return b() + 1 # 定义 a 的时候 b 还不存在
def b():
return 10
print('a() =', a())
# 2) 同名会直接覆盖,Python 没有按参数类型/个数区分的函数重载
def f():
return '第一个'
def f(): # 覆盖掉上面那个
return '第二个'
print('同名覆盖后调用:', f())
# 3) 真想要「不同参数走不同实现」:默认参数 / *args,或 functools.singledispatch
def g(n, times=1):
return n * times
print('默认参数:', g(3), g(3, 4))
def show(x):
return '默认分支: %r' % x
def _(x):
return '整数分支: %d' % x
print('singledispatch:', show(7), '/', show('7'))
# 4) 函数里还能再定义函数(内部函数,闭包和装饰器的基础)
def outer():
def inner():
return 'inner'
return inner()
print('内部函数:', outer())实跑输出:
1
2
3
4
5a() = 11
同名覆盖后调用: 第二个
默认参数: 3 12
singledispatch: 整数分支: 7 / 默认分支: '7'
内部函数: inner
变量作用域
-
块级作用域
Python 没有块级作用域:
if/for/while/try这些语句块里赋值的名字,出了块还能用(下面代码正常运行)。1
2
3
4if True:
name = 'xiaoming'
print(name) # xiaoming —— 块外照样能拿到两个例外要记住:函数(含
lambda)和推导式都有自己独立的作用域,类体也是一个独立命名空间——所以类定义里写[y for _ in range(1)]反而看不到类变量y。 -
局部作用域
局部作用域:函数内部赋值的变量是局部变量,只能在函数内部使用,函数返回后就没了。
1
2
3
4
5
6def test():
a = 10
return a
test()
print(a) # NameError: name 'a' is not defined -
全局作用域
全局变量:定义在函数外部的变量。函数里读它不需要任何声明;但只要在函数里给它赋过值,这个名字在整个函数内都算局部变量。
1
2
3
4
5
6
7
8
9num = 20
def show():
global num # 声明了才能改模块级的那个 num
num = 30
print(num)
show() # 30
print(num) # 30 —— 全局的确实被改了说明:
- 不加
global时函数里的num = ...会新建一个局部变量,全局的不动;而如果先读后写(print(num)在前、num = ...在后),因为赋值让num在整个函数里都是局部的,那一句读会抛UnboundLocalError: local variable 'num' referenced before assignment—— 这不是「优先使用内部的」,而是同一个名字不会一半局部一半全局。 - 想改全局变量必须
global声明;改外层函数的变量用nonlocal(global管不到函数作用域)。 - 少用
global:函数依赖外部状态后不好测试,参数进、返回值出更清楚。
- 不加
-
nonlocal
内层函数要修改外层函数的局部变量时用它(只读的话不用声明)。
1
2
3
4
5
6
7
8
9
10
11
12def wai():
age = 20
def nei():
nonlocal age # 声明改的是外层的 age,不是新建局部变量
age = 30
print(age) # 30
nei()
print(age) # 30 —— 外层看到内层的修改
wai()nonlocal只能绑到外层函数的变量;在模块顶层或没有同名外层变量时用它,直接SyntaxError: no binding for nonlocal。 -
整体示例
把上面这些点全用一遍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74name = '全局的 name'
count = 0
# 1) Python 没有块级作用域:if / for / while / try 里赋的名字,块外照样能拿到
if True:
block_var = 'if 块里定义的'
for k in range(2):
pass
try:
raise ValueError('演示')
except ValueError as err:
caught = 'except 块里定义的 ' + type(err).__name__
print('块外的变量:', block_var, '/', 'for 的循环变量还在:', k, '/', caught)
print('但 except 的 err 在块结束后被自动删除:', 'err' in dir())
# 2) 局部作用域:函数里赋值的名字是局部变量,函数返回就没了
def local_demo():
a = 10
return a
print('函数内部用局部变量:', local_demo())
try:
print(a)
except NameError as e:
print('函数外访问局部变量 ->', type(e).__name__)
# 3) 读全局不用声明;但函数里一旦有赋值,这个名字在整个函数内都是局部的
def read_global():
return name
print('函数里直接读全局:', read_global())
def broken():
print(name) # 下一行有 name = ...,所以这里读的是「还没赋值的局部变量」
name = '局部的 name'
try:
broken()
except UnboundLocalError as e:
print('先读后写 ->', type(e).__name__)
def use_global():
global count # 声明了才能改模块级的那个变量
count += 1
return count
print('global 改全局:', use_global(), ' 模块里的值变成:', count)
# 4) nonlocal 改的是外层函数的局部变量;global 管不到函数作用域
def outer():
x = '外层的 x'
def mid():
nonlocal x
x = '被内层改了'
mid()
return x
print('nonlocal:', outer())
# 5) 推导式和 lambda 有自己独立的作用域,循环变量不外泄
squares = [i * i for i in range(3)]
print('推导式结果:', squares, ' 它的 i 是否泄漏到外面:', 'i' in dir())
# 6) 类体是独立命名空间:类定义里写推导式看不到类变量
class Demo:
y = 4
try:
z = [y for _ in range(1)]
except NameError:
z = 'NameError'
print('类体里推导式看不到类变量:', Demo.z)实跑输出:
1
2
3
4
5
6
7
8
9
10块外的变量: if 块里定义的 / for 的循环变量还在: 1 / except 块里定义的 ValueError
但 except 的 err 在块结束后被自动删除: False
函数内部用局部变量: 10
函数外访问局部变量 -> NameError
函数里直接读全局: 全局的 name
先读后写 -> UnboundLocalError
global 改全局: 1 模块里的值变成: 1
nonlocal: 被内层改了
推导式结果: [0, 1, 4] 它的 i 是否泄漏到外面: False
类体里推导式看不到类变量: NameError
递归函数
-
定义:函数内部调用函数本身的函数叫递归函数
-
组成:
- 函数内部调用自己
- 终止条件
- 核心算法
-
特点:
- 代码简洁,但层数一深可读性就下降(不易理解)。
- 每一层调用都要占一个栈帧,所以内存随深度线性增长;没有终止条件不会「立即」崩,而是一路撞到递归深度上限后抛
RecursionError。 - 这个上限别背数字,要现查
sys.getrecursionlimit():本机命令行跑是 1000,同一份代码在 PyCharm 里运行是 3000(IDE 注入的运行环境会把它调高),换个环境就换个数字。而且它只是「允许多深」,不等于「一定能多深」:实测上限 1000 时真正能递归到 998 层(跑这段测试时栈上已经有帧了),把上限设成 3000 时能到 2998 层。 sys.setrecursionlimit()可以调,但调太大只是把RecursionError换成更狠的崩溃:栈是操作系统分给进程的真实资源,超过栈大小会直接段错误(segfault),连异常都拿不到。- Python 没有尾调用优化,尾递归不会退化成循环,深递归场景改写成迭代更稳。
- 有些安全关键领域的编码规范直接禁用递归(如汽车电子的 MISRA C:禁止递归、禁止动态内存)。
- 实用建议:树形结构、目录遍历、分治这类「天生递归」的问题用它最自然;能用循环清楚表达的(求和、计数)就别递归。递归慢或重复计算多时,加
functools.lru_cache记忆化往往就够了。
-
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76import functools
import os
import sys
# 1) 阶乘:终止条件 + 调用自己(文末练习第 1 题)
def fact(n):
if n <= 1: # 终止条件
return 1
return n * fact(n - 1) # 调用自己
print('fact(5) =', fact(5))
# 2) 斐波那契(文末练习第 2 题):朴素递归会重复计算,n 一大就慢
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print('fib(20) =', fib(20))
# 3) 加个记忆化装饰器,同样的递归从指数级降到线性
def fib_cached(n):
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
print('fib_cached(60) =', fib_cached(60))
print('确实命中过缓存:', fib_cached.cache_info().hits > 0)
# 4) 迭代版更稳:Python 没有尾调用优化,递归层数深就是风险
def fib_loop(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print('迭代与记忆化结果一致:', fib_loop(60) == fib_cached(60))
# 5) 没有终止条件不是「立即」崩,而是撞到递归深度上限
print('本机递归上限(换环境就不同,别背数字):', sys.getrecursionlimit())
def probe(n=0): # 没有终止条件的递归:一路到底,看实际能到几层
try:
return probe(n + 1)
except RecursionError:
return n
print('无限递归撞到上限 -> 实际能到:', probe())
# 6) 递归真正自然的场景:树形结构 / 目录遍历
os.makedirs('tree/a/b', exist_ok=True)
for p in ('tree/top.txt', 'tree/a/mid.txt', 'tree/a/b/low.txt'):
with open(p, 'w') as fp:
fp.write('x')
def walk(root):
total = 0
for entry in sorted(os.listdir(root)):
path = os.path.join(root, entry)
if os.path.isdir(path):
total += 1 + walk(path) # 目录:自己进去继续数
else:
total += 1 # 文件:数一个
return total
print('tree 下 2 个子目录 + 3 个文件 =', walk('tree'))
# 7) 汉诺塔:三层正好 2**3 - 1 步
def hanoi(n, src='A', dst='C', via='B'):
if n == 0:
return []
return hanoi(n - 1, src, via, dst) + [(n, src, dst)] + hanoi(n - 1, via, dst, src)
steps = hanoi(3)
print('汉诺塔 3 层步数:', len(steps), ' 第一步:', steps[0], ' 最后一步:', steps[-1])实跑输出:
1
2
3
4
5
6
7
8
9fact(5) = 120
fib(20) = 6765
fib_cached(60) = 1548008755920
确实命中过缓存: True
迭代与记忆化结果一致: True
本机递归上限(换环境就不同,别背数字): 1000
无限递归撞到上限 -> 实际能到: 998
tree 下 2 个子目录 + 3 个文件 = 5
汉诺塔 3 层步数: 7 第一步: (1, 'A', 'C') 最后一步: (1, 'A', 'C')
练习
1 | 1.求阶乘 |