前言
本文基于我的个人笔记,深入总结函数的各种参数类型、返回值本质、函数作为对象、闭包、变量作用域、递归以及匿名函数,并融入了我自己在学习过程中的理解和思考。希望能帮你彻底搞懂函数的方方面面。
一、函数的参数
1.1 函数的参数设计(我的理解)
在学习函数时,我一直纠结一个问题:函数有 return 和没有 return 到底有什么区别?
经过反复思考和验证,我得出以下结论:
-
函数一定会有 return,因为 return 关键字表示函数执行完毕。即使你没有在函数体内写 return,Python解释器也会在函数末尾自动添加一个 return None。
-
区别在于 return 之后有没有结果:有 return 值 则返回该值,否则返回 None。
我的理解:
return 是一个函数的“结束标志”,它总是存在,只是有时它悄悄地带回了 None,有时它光明正大地带回你想要的结果。
注意:函数遇到 return 会立即结束,后面的代码不再执行。函数调用几次,函数体就执行几次。
1.1.1 没有传入参数也没有回传值的函数
如果函数没有返回值,调用后不需要接收结果,如果非要接收,得到的是 None。
示例:
def greeting():
print("Hello World")
# 程序会在末尾自动添加 return None
r = greeting()
print(r) # None
1.1.2 传递多个参数
调用有返回值的函数时,一定要接收返回的结果。
示例:
def greeting(name: str, age: int) -> bool:
name = name.title()
if age >= 18:
print(f"大家好,我叫{name},今年{age}岁,成年了。")
return True
else:
print(f"大家好,我叫{name},今年{age}岁,未成年。")
return False
adult = greeting('zl', 18) # 必须接收返回值
if adult:
print("今晚去98嗨!")
1.1.3 传递任意数量的参数(*args)
当不确定会有多少个位置参数传入时,使用 *args 将多余参数打包成元组。
示例:
def math_sum(*args):
print(args, type(args)) # 元组类型
total = 0
for arg in args:
total += arg
return total
print(math_sum(1, 2, 3, 4, 5)) # 15
print(math_sum()) # 0
1.1.4 含有一般参数与任意数量的关键词参数(**kwargs)
使用 **kwargs 接收任意数量的关键字参数,它们会以字典形式存在。
示例:
def stu_info(name: str, age: int, **kwargs) -> dict:
print(kwargs, type(kwargs)) # 字典
# 这里可以根据 kwargs 做更多处理
level = 'A'
return {'name': name, 'level': level}
r = stu_info('zl', 18, chinese=100, math=90, english=59)
print(r)
查看函数文档注释:print(stu_info.__doc__)
参数组合顺序:位置参数 → 默认参数 → *args → 关键字参数(**kwargs)
示例:
def func(a, b, c, *args, x=666, y=888, z=999, **kwargs):
print(a, b, c, args, x, y, z, kwargs)
1.2 函数回传值
1.2.1 回传 None
-
None 在Python中是一个特殊值,类型为 NoneType,作为布尔值使用时视为 False。
-
注意:False 并不是 None,空列表、空字典等也是 False,但都不是 None。
1.2.2 回传多个数据的应用(实质是返回元组)
函数可以返回多个值,用逗号分隔,实际返回的是一个元组。
示例:
def math_operation(x: int, y: int):
add = x + y
sub = x – y
mul = x * y
div = x // y
mod = x % y
return add, sub, mul, div, mod # 返回元组
result = math_operation(10, 3)
print(result) # (13, 7, 30, 3, 1)
也可以返回字典,便于通过键名取值:
def math_operation(x, y):
return {'add': x+y, 'sub': x-y, 'mul': x*y, 'div': x//y, 'mod': x%y}
result = math_operation(10, 3)
print(result['mod']) # 1
1.3 函数是一个对象
在Python中,一切皆对象,函数也不例外。函数名本质上是一个指向函数对象的变量(内存地址)。
注意:
-
打印函数名不加括号,得到的是内存地址:print(greeting)。
-
可以将函数赋值给另一个变量,两个变量指向同一个函数对象。
示例:
def greeting(name='Jack'):
print(f'Hello {name}!')
a = greeting # a 指向 greeting 函数的地址
print(a) # 打印地址
a() # 调用 a() 相当于调用 greeting()
a('Rose')
我的理解:
对象的本质是地址,函数的名字就是一个地址。函数名可以反复赋值,就像普通变量一样。有返回值时记得接收。
1.4 函数可以当作参数传递给其他函数
因为函数是对象,所以它可以作为参数传递给另一个函数。这种用法在回调函数、装饰器等场景很常见。
示例:
def running(func, arg1, arg2):
print("执行前…")
r = func(arg1, arg2)
print(f"结果:{r}")
print("执行后…")
def add(x, y):
return x + y
def mul(x, y):
return x * y
running(add, 10, 2)
running(mul, 10, 2)
注意:传递函数时不要加括号,否则就变成了调用函数并传递返回值。
二、闭包
定义:在内层函数中使用了外层函数的变量,这样的语法称为“闭包”。
我的理解:
闭包就像是一个“动态生成的函数”,它记住了外层函数的环境变量,即使外层函数已经执行完毕,这些变量依然被内层函数保留。
示例:一次函数 f(x) = kx + b
示例:
def outer(k, b):
def inner(x):
return k * x + b
return inner
func1 = outer(5, 10) # 创建 f(x) = 5x + 10
print(func1(3)) # 25
func2 = outer(3, 15) # f(x) = 3x + 15
print(func2(3)) # 24
闭包让函数更加灵活,可以“定制”行为。
三、延伸重点
3.1 局部变量与全局变量
-
局部变量:定义在函数内部,只能在函数内使用,函数执行结束即销毁。
-
全局变量:定义在函数外部,整个程序都能访问,程序结束才销毁。
注意:在函数内不能直接修改全局变量的值(除非使用 global 声明)。
3.2 locals() 和 globals()
-
locals():以字典形式返回当前局部作用域的所有变量。
-
globals():以字典形式返回全局作用域的所有变量。
示例:
msg = "全局变量"
def func():
a = 666
print(locals()) # {'a': 666}
func()
print(globals()) # 包含 msg 等全局变量
3.3 局部函数和全局函数
-
在函数内部定义的函数称为局部函数,外部无法直接访问。
-
主程序入口通常用 if __name__ == '__main__': 包裹,表示只有直接运行此文件时才执行。
3.4 局部变量与全局变量使用相同的名称
-
如果局部变量和全局变量同名,在函数内遵循就近原则,使用的是局部变量。
-
在函数外使用的是全局变量。
-
若想在函数内修改全局变量,需用 global 声明。
示例:
x = 10
def func():
x = 666 # 局部变量,不影响全局
print(x) # 666
func()
print(x) # 10
def func2():
global x
x = 888 # 修改全局变量
func2()
print(x) # 888
3.5 nonlocal 变量(非局部)
nonlocal 用于嵌套函数中,声明变量不是当前函数的局部变量,而是上一层函数的变量。
我的理解:
在嵌套函数中,如果内层函数想要修改外层函数的变量,就需要用 nonlocal 声明。global 是修改最顶层的全局变量,而 nonlocal 是修改上一层的局部变量。
示例对比:
未使用 nonlocal:
def outer():
z = 666
def inner():
z = 888 # 新的局部变量
print("inner:", z)
inner()
print("outer:", z)
outer()
# inner: 888
# outer: 666
使用 nonlocal:
def outer():
z = 666
def inner():
nonlocal z # 声明使用外层函数的 z
z = 888
print("inner:", z)
inner()
print("outer:", z)
outer()
# inner: 888
# outer: 888
注意:nonlocal 只能用于多层嵌套函数中,且变量必须存在于外层函数中。
四、递归
递归:函数调用自身。
我的理解:
递归就像俄罗斯套娃,一层套一层,直到最小的那个不能再套(基本结束条件),然后一层层返回结果。递归的过程可以理解为“压栈”和“弹栈”。
无限递归(递而不归)会导致栈溢出错误,所以必须设置结束条件。
示例:
def func(n):
if n == 0: # 结束条件
return
print(n)
func(n-1) # 逼近结束条件
func(10)
4.1 递归“三定律”
递归算法必须有一个基本结束条件(最小规模问题的直接解决)。
递归算法必须能改变状态向基本结束条件演进(减小问题规模)。
递归算法必须调用自身(解决减小了规模的相同问题)。
4.2 求阶乘
示例:
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
print(factorial(5)) # 120
执行过程分析:
-
压栈:factorial(5) → factorial(4) → factorial(3) → factorial(2) → factorial(1)
-
弹栈:factorial(1) 返回 1 → factorial(2) 返回 2×1=2 → … → factorial(5) 返回 120
4.3 斐波那契数列
问题:一对兔子,从第三个月开始每月生一对,问第 n 个月有多少对?
递归定义:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)
直接递归(效率低):
def fib(n):
if n <= 2:
return 1
return fib(n-1) + fib(n-2)
优化:使用备忘录(字典)缓存结果:
memo = {}
def fib(n):
if n <= 2:
return 1
if n not in memo:
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
print(fib(63)) # 快速得出结果
我的理解:
未优化的递归会重复计算很多次,比如 fib(5) 需要计算两次 fib(3)。用字典存下已经算过的值,能极大提高效率,这就是“备忘录”思想。
4.4 汉诺塔
经典递归问题:将 n 个盘子从 A 柱借助 B 柱移动到 C 柱,每次只能移动一个盘子,且大盘不能在小盘上面。
示例:
def hanoi(n, f, a, t):
if n == 1:
print(f'第1个盘: {f} –> {t}')
return
hanoi(n-1, f, t, a) # 将 n-1 个盘子从 f 借助 t 移到 a
print(f'第{n}个盘: {f} –> {t}')
hanoi(n-1, a, f, t) # 将 n-1 个盘子从 a 借助 f 移到 t
hanoi(3, 'A', 'B', 'C')
输出:
第1个盘: A –> C
第2个盘: A –> B
第1个盘: C –> B
第3个盘: A –> C
第1个盘: B –> A
第2个盘: B –> C
第1个盘: A –> C
我的理解:
汉诺塔的核心思想是:先把上面 n-1 个盘子移到辅助柱,再把最大的盘子移到目标柱,最后把 n-1 个盘子从辅助柱移到目标柱。递归完美体现了“分而治之”的思想。
五、匿名函数 lambda
lambda 用于创建简单的、一行表达式的匿名函数。
语法:lambda 参数: 表达式
示例:
add = lambda x, y: x + y
print(add(3, 5)) # 8
# 常用于排序、map、filter等
pairs = [(1, 'one'), (2, 'two'), (3, 'three')]
pairs.sort(key=lambda pair: pair[1]) # 按单词排序
print(pairs)
注意:lambda 只能包含一个表达式,不能包含语句(如赋值、循环等)。
总结
本文从参数传递、返回值、函数对象、闭包、作用域、递归到匿名函数,全面梳理了函数相关的核心知识点,并融入了我个人在学习过程中的理解和思考。希望这篇文章能帮你更深刻地理解函数,写出更优雅的代码。
如果你觉得有帮助,欢迎点赞、收藏、交流!



