1. 什么是函数?
函数是组织在一起的代码块,用于完成特定任务,函数是封装思想的体现,将一道复杂的流程打包,使用时直接调用,无需重复编写,无需关心具体实现方式,直接使用。
1.1 函数的特点
- 封装:将复杂逻辑封装在函数中,对外提供简单的接口。
- 复用:定义一次,调用多次,避免重复编写相同逻辑。
- 逻辑清晰:提高代码的可读性与维护性。
例如,你想计算圆的面积,虽然圆有无穷多个,但是计算方法都是一样的,如果我们将计算面积的相关代码打包为函数后,每次计算就不用重复造轮子,需要用时直接调用即可。
import math
# 定义函数
def circle_area(radius):
"""计算圆的面积"""
if radius < 0:
return "半径不能为负"
return math.pi * radius ** 2
在这个例子中,我们可以发现函数通过 def 关键字定义,基本结构如下:
def function_name(parameters):
"""函数文档说明,可选"""
# 函数体:执行的逻辑代码
return result # 返回结果,可选
这样,我们就将一套具体的过程抽象成了一个函数circle_area(),来实现圆面积的计算
1.2 调用函数
而我们想要使用也很简单,写出函数名加括号就能使用,一些函数需要传入参数,如计算圆的面积需要知道半径。
# 调用函数
area = circle_area(5)
print(f"半径为 5 的圆的面积是 {area:.2f}")
我们可以用 area这个变量来接收计算出的结果,当然,直接调用也是可以的。
需要注意的是,函数中的代码只有在调用后才会运行,没有调用的函数是不会运行的。
1.3 函数的参数
1.3.1 位置参数与关键字参数
参数有两种基本的使用方式,一种是按照参数的顺序传入参数,另一种是按照参数名。
- 位置参数:根据参数顺序传递值。
- 关键字参数:通过指定参数名传递值,顺序可以不固定。
示例:
def greet(name, age):
print(f"你好,{name}!你今年 {age} 岁了。")
# 使用位置参数
greet("小明", 18)
# 使用关键字参数
greet(age=22, name="小红")
不难发现,用参数顺序写出来的代码更简洁,但是顺序不可以出错;而用关键字参数增强了代码的可读性,尤其在参数较多时,顺序很容易忘记,使用关键词更方便
1.3.2 默认参数
默认参数:顾名思义,为参数指定默认值,调用函数时可以选择不传递该参数。
示例:
def introduce(name, country="中国"):
print(f"你好,我是{name},来自{country}。")
introduce("小明") # 使用默认值
introduce("John", "美国") # 覆盖默认值
默认参数提供了合理的默认行为,减少了函数调用的复杂度。
1.3.3 可变参数
可变参数指的是不限制参数数量的一种形式。
***args**:接收任意数量的位置参数,存储为元组。****kwargs**:接收任意数量的关键字参数,存储为字典。
示例:
def add(*args):
"""多项求和"""
return sum(args)
def print_info(**kwargs):
"""打印任意信息"""
for key, value in kwargs.items():
print(f"{key}: {value}")
# 调用函数
print(add(1, 2, 3, 4, 5))
print_info(name="小明", age=18, hobby="编程")
可变参数提升了函数的扩展性,适用于处理不确定参数数量的场景。
1.4 返回值
函数可以通过 return 返回结果,返回值也可以为空,根据函数的功能确定。
拿我们计算圆的面积的例子来说,就需要返回面积的值。而如果我们的函数只是用来输出一段话,就不需要返回值。
- 单个值:返回一个具体结果。
- 多个值:使用元组返回多个结果。
示例:
def find_extremes(numbers):
"""返回最大值和最小值"""
return max(numbers), min(numbers)
max_num, min_num = find_extremes([2, 8, 3, 7, 4])
print(f"最大值:{max_num}, 最小值:{min_num}")
2. 作用域与全局变量
全局变量的概念我们在 2.1 变量那节简单介绍过,本节结合函数就其作用域再详细说明。
2.1 局部变量
局部变量是定义在函数内部的变量,只能在函数内部使用。
函数执行完毕后,局部变量会被销毁,不会影响外部变量。
特点:
- 作用范围:局限于函数内部。
- 生命周期:函数执行完毕后立即销毁。
- 安全性:不会污染全局变量。
示例:
def greet():
message = "Hello, Python!" # 局部变量
print(message)
greet()
# print(message) # 会报错,因为 message 是局部变量,函数外部无法访问
message 是局部变量,仅在 greet() 函数内部有效,函数执行完毕后,这个变量就不存在了。
2.2 全局变量
全局变量是定义在函数外部的变量,可以在整个程序中使用。
所有函数都能访问它,但函数默认不能修改它。
特点:
- 作用范围:贯穿整个程序。
- 生命周期:直到程序结束才被销毁。
- 风险性:使用不当可能导致意外的值覆盖。
例子:
count = 0 # 全局变量
def increment():
print("全局变量 count:", count)
increment()
输出:
全局变量 count: 0
count 是全局变量,函数内部直接访问它。
2.3 作用域
作用域是指变量在程序中的可见范围,分为局部作用域和全局作用域。
局部作用域(Local Scope):局部变量只能在函数内部使用。
全局作用域(Global Scope):全局变量可以在所有地方使用,但函数中默认无法修改。
像下面这个例子,虽然我们有两个变量名都为x的变量,但因其作用域不同,他们并不冲突。
x = 10 # 全局变量
def test_scope():
x = 5 # 局部变量
print("函数内部的 x:", x)
test_scope()
print("函数外部的 x:", x)
输出:
函数内部的 x: 5
函数外部的 x: 10
函数内部的 x 是局部变量,不会影响全局变量 x 的值。
在函数内部,如果直接给一个全局变量赋值,Python 会将其视为局部变量。如果确实需要修改全局变量的值,必须显式使用 global 关键字。
使用 global 关键字可以显式声明某个变量是全局变量,允许函数内部修改全局变量的值。
示例:
z = 300 # 全局变量
def change_global():
global z # 声明 z 为全局变量
z = 400 # 修改全局变量的值
print("函数内部修改后的 z:", z)
change_global()
print("函数外部的 z:", z)
输出:
函数内部修改后的 z: 400
函数外部的 z: 400
简单来理解,函数中的变量的作用域仅在本函数中,函数相当于一个黑箱,黑箱内部与外部互不干扰,即便他们的变量名一样,但实际上不是一个东西,除非使用 global 关键字去声明。
3. 函数的高级用法
3.1 函数作为参数
函数的参数不仅可以传递一般的数据类型,也可以传一个一个函数。
比如两个数进行四则运算,我们可以传入一个函数来指定两数之间的运算符号。
def apply(func, x, y):
"""对两个数应用函数"""
return func(x, y)
def multiply(a, b):
return a * b
def plus(a, b):
return a * b
result = apply(multiply, 3, 4)
print(f"结果:{result}")
3.2 匿名函数(lambda)
匿名函数(Lambda 函数)是一种无需显式定义函数名的简化函数形式,简单来说就是没有名字的函数,通常是在代码中临时定义并使用的一个函数表达式。
格式:
lambda 参数: 返回值
例如我们需要计算一下平方数,又不想单独写一个完整的平方数计算的函数。
squared = lambda x: x ** 2
print(squared(5))
3.3 嵌套函数
嵌套函数与嵌套循环类似,是在函数内部定义的函数。一般用于隐藏逻辑,让内部函数无法被外部直接访问。或者是将逻辑分层,将复杂逻辑拆分为多个小模块。
示例:
def calculator(operation):
def add(a, b):
return a + b
def multiply(a, b):
return a * b
if operation == "add":
return add
elif operation == "multiply":
return multiply
# 调用嵌套函数
add_function = calculator("add")
print(add_function(3, 4)) # 输出:7
使用nonlocal关键字,可以使嵌套函数使用函数内部的局部变量,这其实涉及到了闭包(嵌套函数可以捕获其外部函数中的变量,形成闭包)。
3.4 递归函数
递归函数是指在函数内部调用自身的一种函数形式(自己调用自己),递归的使用非常常见,如果学过数据结构的小伙伴不会陌生,二叉树的变量就通常可以用递归实现。
递归问题的解决通常需要满足两个条件:
- 递归基例(Base Case):明确递归结束的条件,否则会陷入无限循环。
- 递归关系(Recursive Relation):定义问题如何分解为子问题的方式。
我们来看一个经典的例子,计算一个数的阶乘 n!。
def factorial(n):
if n == 1: # 基线条件
return 1
return n * factorial(n - 1) # 递归步骤
print(factorial(5)) # 输出:120
5!的计算是 12345,例子中则是从 5 开始,每次调用自身,但是每次的值都减 1,累乘到 1 结束。
递归本质上是一种“分而治之”的思路。
将一个复杂问题拆分成相似的更小子问题,直到问题可以被直接解决。
递归调用时,系统会为每次函数调用分配一个栈帧(Stack Frame)来保存函数的状态和局部变量。
这里用王道的图帮助大家理解。
函数调用时,每一次递归都会被“压入栈中”,直到遇到基例停止递归,之后“出栈”计算结果。
factorial(5)调用factorial(4),将5和其状态保存到栈。factorial(4)调用factorial(3),将4和其状态保存到栈。- 如此反复,直到
factorial(1)返回1,此时递归停止。 - 递归出栈,计算结果:
factorial(2) = 2 * 1factorial(3) = 3 * 2factorial(4) = 4 * 6factorial(5) = 5 * 24
最终结果为 120。
我们总结一下递归函数的优缺点。
优点:代码简单,递归让逻辑清晰,尤其在处理树结构、分治问题等场景时非常有用。
缺点:效率低,递归调用会产生大量栈帧,特别是没有优化的情况下(如斐波那契),容易造成栈溢出。内存开销大,每次递归调用都需要额外的内存来保存上下文状态。
4. 小结
函数在编程中的应用非常广泛,函数通过封装复杂逻辑,实现代码复用和清晰化。良好的函数设计应关注单一职责,即每个函数只完成一件事,便于维护和扩展。
这种封装的思想不仅减少了重复代码,还通过清晰的输入输出接口,将复杂逻辑隐藏在内部,实现模块化设计,降低了耦合度。
本节中一些常见易错点🤔:
⚠️默认参数赋值问题:默认参数为可变对象(如列表、字典)时,可能导致数据共享问题,应使用不可变对象(如 None)。
⚠️参数位置:调用函数时,先写位置参数,再写关键字参数,避免顺序错误。
⚠️作用域问题:修改全局变量时需显式使用 global,但频繁使用全局变量会降低代码的可读性和安全性。