Python入门教程丨2.4 函数、封装、递归

195 阅读10分钟

1. 什么是函数?

函数是组织在一起的代码块,用于完成特定任务,函数是封装思想的体现,将一道复杂的流程打包,使用时直接调用,无需重复编写,无需关心具体实现方式,直接使用。


1.1 函数的特点

  1. 封装:将复杂逻辑封装在函数中,对外提供简单的接口。
  2. 复用:定义一次,调用多次,避免重复编写相同逻辑。
  3. 逻辑清晰:提高代码的可读性与维护性。

例如,你想计算圆的面积,虽然圆有无穷多个,但是计算方法都是一样的,如果我们将计算面积的相关代码打包为函数后,每次计算就不用重复造轮子,需要用时直接调用即可。

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 递归函数

递归函数是指在函数内部调用自身的一种函数形式(自己调用自己),递归的使用非常常见,如果学过数据结构的小伙伴不会陌生,二叉树的变量就通常可以用递归实现。
递归问题的解决通常需要满足两个条件:

  1. 递归基例(Base Case):明确递归结束的条件,否则会陷入无限循环。
  2. 递归关系(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)来保存函数的状态和局部变量。

这里用王道的图帮助大家理解。

函数调用时,每一次递归都会被“压入栈中”,直到遇到基例停止递归,之后“出栈”计算结果。

  1. factorial(5) 调用 factorial(4),将 5 和其状态保存到栈。
  2. factorial(4) 调用 factorial(3),将 4 和其状态保存到栈。
  3. 如此反复,直到 factorial(1) 返回 1,此时递归停止。
  4. 递归出栈,计算结果:
  • factorial(2) = 2 * 1
  • factorial(3) = 3 * 2
  • factorial(4) = 4 * 6
  • factorial(5) = 5 * 24

最终结果为 120


我们总结一下递归函数的优缺点。

优点:代码简单,递归让逻辑清晰,尤其在处理树结构、分治问题等场景时非常有用。

缺点效率低,递归调用会产生大量栈帧,特别是没有优化的情况下(如斐波那契),容易造成栈溢出。内存开销大,每次递归调用都需要额外的内存来保存上下文状态。


4. 小结

函数在编程中的应用非常广泛,函数通过封装复杂逻辑,实现代码复用和清晰化。良好的函数设计应关注单一职责,即每个函数只完成一件事,便于维护和扩展。

这种封装的思想不仅减少了重复代码,还通过清晰的输入输出接口,将复杂逻辑隐藏在内部,实现模块化设计,降低了耦合度。

本节中一些常见易错点🤔:

⚠️默认参数赋值问题:默认参数为可变对象(如列表、字典)时,可能导致数据共享问题,应使用不可变对象(如 None)。

⚠️参数位置:调用函数时,先写位置参数,再写关键字参数,避免顺序错误。

⚠️作用域问题:修改全局变量时需显式使用 global,但频繁使用全局变量会降低代码的可读性和安全性。