Python从入门到实战(四):函数基础与模块化编程 目录一、模块化编程与函数概述1. 为什么需要函数2. 函数与代码复用3. Python 函数的分类二、函数的定义、调用与交互1. 函数的基本语法2. 函数案例3. 参数传递3.1 位置参数与关键字参数3.2 限制参数传递3.3 参数默认值缺省参数4. 可变参数5. 函数返回值三、函数说明文档1. Docstring 的语法标准2. 运行时查阅2.1 使用内置函数 help()2.2 访问属性 __doc__四、变量作用域1. 命名空间与作用域界定2. 作用域遮蔽与 global3. 变量的生命周期五、嵌套与递归1. 函数的嵌套调用2. 递归调用经典递归案例数学阶乘计算3. 递推与回归综合案例简易计算器与菜单系统总结一、模块化编程与函数概述在前面的章节中我们编写的代码大多是线性、扁平化的。当程序规模较小时这种方式清晰直观但随着业务逻辑的日趋复杂如果继续将所有代码一股脑地堆砌在主程序中将会带来巨大的维护灾难。为了解决这一问题我们引入了模块化编程的核心工具——函数1. 为什么需要函数在没有函数之前如果程序中有多处需要执行相同的逻辑例如在多个页面对用户的输入进行格式化校验开发者只能选择将这段代码复制粘贴到所有需要的地方这种做法存在两个致命缺陷代码冗余相同的代码块在程序中大量重复导致源文件体积虚胖严重消耗阅读精力维护成本极高一旦该段逻辑发生变更例如校验规则发生了调整开发者必须手动修改每一处复制的代码。只要遗漏了一处就会直接产生程序漏洞函数的出现正是为了解决这一痛点。通过将这些具有独立功能的代码块打包并命名即可实现代码的模块化2. 函数与代码复用函数Function是组织好的、可重复使用的、用来实现单一或相关联功能的代码段从工程角度来看函数可以被视作一个黑盒输入Input通过参数将外部数据投递进函数内部处理Process在内部执行预设的、封装好的逻辑计算输出Output将最终计算出的数据结果通过返回值返回给调用者作为函数的调用者我们不需要关心内部的具体实现细节只需要明确 函数名、传什么参数 以及 返回什么结果 即可。这种高内聚、低耦合的设计实现了 一次编写到处复用 的目标3. Python 函数的分类在 Python 的实际开发中根据函数的来源与提供形式我们可以将函数清晰地划分为以下三大类分类说明举例内置函数由 Python 官方原生提供无需任何导入操作直接写出函数名即可使用print()、type()、range()、int()模块提供的函数已经由他人或官方编写好并打包在特定模块中需要先导入指定的模块后才能通过 “模块名。函数名” 的形式调用math.sqrt ()计算平方根、math.floor ()向下取整自定义函数程序员自己根据特定业务需求定义的函数。遵循 “先定义、再使用” 的原则具体写法将在下一小节详细讲解开发者自行声明的计算函数、业务校验函数等二、函数的定义、调用与交互理解了函数的分类后我们开始学习如何构建一个自定义函数1. 函数的基本语法在 Python 中声明一个自定义函数需要使用 defdefine的缩写关键字。其标准语法结构如下def 函数名(形式参数1, 形式参数2, ...): 函数说明文档可选 函数体具体实现的业务逻辑代码 return 返回值def关键字用于告诉 Python 解释器这里正在声明一个函数函数名遵循小写字母与下修划线组合的命名规范需做到见名知意参数列表写在小括号内用于接收外部传入的数据。如果没有参数括号也不能省略冒号与缩进冒号:表示函数体代码块的开始函数体内部的所有代码必须保持统一的缩进2. 函数案例我们来实现一个简单的打招呼函数。这个函数接收一个人的名字并在控制台打印一行欢迎语# 1. 函数的定义此时函数体内的代码并不会执行 def greet_user(username): print(f你好{username}欢迎学习 Python 编程。) # 2. 函数的调用此时程序才会真正执行函数体内部的代码 greet_user(张三) greet_user(李四)函数的执行流程剖析当 Python 解释器运行上述程序时其执行流如下3. 参数传递参数是函数与外部进行数据交互的关键桥梁形参与实参形式参数在定义函数时括号里写的变量名如上面案例中的 username。它只是一个占位符在函数未被调用时它不占用实际的内存空间实际参数在调用函数时真实传给函数的数据如上面案例中的 张三。它是具有具体确定的类型和值的实体3.1 位置参数与关键字参数在调用一个带有多个参数的函数时Python 提供了两种数据传递方式位置参数严格按照定义时参数的先后顺序依次传入实参和形参按位置一一对应关键字参数在调用时显式指定 形参名 实参值。这种方式可以打破参数传递的顺序限制提高代码可读性# 定义一个展示个人信息的函数 def show(name, age): print(f姓名{name}年龄{age}) # 方式 A位置参数传递严格一一对应 show(王五, 20) # 方式 B关键字参数传递顺序可颠倒依然能准确映射 show(age25, name赵六)在混合使用位置参数和关键字参数时位置参数必须写在关键字参数的前面。否则Python 会抛出语法错误 SyntaxError: positional argument follows keyword argument3.2 限制参数传递在 Python 3.8 之后为了让函数的接口定义更加严谨Python 引入了 / 和 * 两个特殊的符号作为参数列表中的边界隔离强制位置参数符号在参数列表中写在斜杠 /左边的参数在函数调用时必须使用位置参数的形式传递绝对不允许写出参数名def floor_divide(a, b, /): return a // b # 正确调用只能传值 floor_divide(10, 3) # 错误示范一旦写了变量名就会抛出 TypeError # floor_divide(a10, b3)强制关键字参数符号在参数列表中写在星号 * 右边的参数在函数调用时必须使用关键字参数的形式传递强行要求写出变量名def send_email(title, *, content): print(f发送邮件{title}内容{content}) # 正确调用content 必须加上变量名 send_email(开会通知, content下午两点在 3 楼会议室。) # 错误示范content 若直接传值会抛出 TypeError # send_email(开会通知, 下午两点在 3 楼会议室。)3.3 参数默认值缺省参数在定义函数时可以为某些参数指定一个默认值。如果在调用函数时没有为该参数传值函数就会自动采用该默认值。# 为 country 参数设置默认值 中国 def set_profile(name, country中国): print(f姓名{name}国籍{country}) set_profile(阿里) # 未传第二个参数输出姓名阿里国籍中国 set_profile(Tom, 美国) # 传入新值覆盖默认值输出姓名Tom国籍美国4. 可变参数在定义函数时如果我们无法预先确定调用者到底会传入多少个参数就需要用到可变参数又称不定长参数。Python 提供了两种形式来应对不同的数据结构可变位置参数*args在形参名前加上一个星号这就代表该参数可以接收任意数量的、零散的位置参数底层机制Python 会将用户传入的所有多余的位置参数自动打包成一个元组# 定义一个可以计算任意多个数字之和的函数 def add_all(*args): # 此时 args 接收到的是一个数字包裹 total 0 for num in args: total num return total # 调用时可以传任意个数字 print(add_all(1, 2, 3)) # 输出6 print(add_all(10, 20, 30, 40)) # 输出100可变关键字参数kwargs在形参名前加上两个星号 用于接收任意数量的、显式命名的关键字参数底层机制Python 会将这些多余的键值对参数自动打包成一个字典即一种带有名字和对应值的 包裹# 定义一个记录用户任意扩展信息的函数 def print_user_profile(name, **kwargs): print(f核心用户{name}) # kwargs 接收到的是一个键值对包裹 for key, value in kwargs.items(): print(f扩展信息 - {key}: {value}) # 调用时可以随意传入键值对 print_user_profile(张三, age18, city北京, jobEngineer)5. 函数返回值函数不仅可以接收数据还可以向外部输出处理结果为什么需要返回值在前面的案例中我们在函数内部直接使用了 print() 打印结果。但在实际的工业级开发中函数计算出的数据往往需要参与到下一步的业务中例如计算出商品的折扣价后还需要拿这个价格去计算用户的账户余额是否足够return 关键字就是用来将函数内部的计算结果返回调用处的工具# 定义一个带有返回值的计算函数 def add_numbers(a, b): result a b return result # 将计算出的结果投递出去 # 调用函数并用一个变量接收其返回值 sum_value add_numbers(10, 20) # 此时 sum_value 成功拿到了数值 30可以参与后续计算 final_price sum_value * 0.8 print(final_price)return 的两种用法数据投递将执行结果交给调用者立即终止一旦函数体执行到 return 语句当前函数会立即结束并强制退出。无论 return 后面还有多少行代码统统不再执行def check_test(score): if score 60: return 不及格 return 及格 print(这句话永远不会被执行) # 位于 return 之后属于死代码隐式返回值在 Python 中如果一个函数内部没有写 return语句或者只是单独写了一个 return 而没带任何数据Python 都会在函数结束时隐式地返回一个特殊的对象NoneNone 是 Python 的一个内置常量代表 空 或 什么都没有其数据类型是 class NoneTypedef no_return_func(): print(我里面没有写 return) value no_return_func() print(value) # 输出None三、函数说明文档编写可读性高的代码是基本要求。当你独立编写了一个复杂的自定义函数后如果不加任何注释说明其他团队成员可能需要耗费大量精力去走读每一行源码才能搞懂使用方法。为此Python 引入了文档字符串机制1. Docstring 的语法标准Docstring文档字符串是指直接写在函数体第一行、用三引号 包裹的特殊文本。与普通的单行注释不同Docstring 具有固定的语法结构用于集中阐述函数的功能描述、入参规范以及返回值含义在工业级开发中最广为使用的当属 Google 风格的编写规范def calculate_tax(income, tax_rate0.03): 计算个人所得税。 根据用户的总收入和对应的税率计算出应缴税额。 Args: income: 浮点数或整型代表用户的月度总收入。 tax_rate: 浮点数代表个税税率默认为 0.03。 Returns: 浮点数计算得出的最终应纳税额。 if income 0: return 0.0 return income * tax_rate2. 运行时查阅编写了 Docstring 的函数其文档信息并不会随程序编译而消失而是会被 Python 解释器保存在该函数的内置属性 __doc__ 中。我们可以通过以下两种方式在不需要查看源代码的情况下动态了解一个函数2.1 使用内置函数 help()调用 help(函数名) 可以直接在控制台中以交互式手册的形态打印出该函数的详细文档# 查看上面自定义函数的说明书注意只需传函数名不要加括号 help(calculate_tax)2.2 访问属性 __doc__如果只需要提取文档字符串的原始文本可以直接访问该属性。print(calculate_tax.__doc__)编写规范无论函数内的业务多么简单都建议养成至少书写一行 Docstring 简述功能的习惯。函数说明文档应当言简意赅准确描述 做什么而不是 内部逻辑 怎么做四、变量作用域当代码被拆分进不同的模块和函数后变量的管理也面临新的规则在函数内部声明的变量在函数外部还能访问吗为了解答这一疑问我们必须引入作用域和生命周期的概念1. 命名空间与作用域界定作用域是指程序中变量、函数和对象名称的可访问范围。一个变量在哪些代码区域是合法的、可以被直接读取或修改的完全由它所处的作用域决定在基础开发中我们最核心需要区分的是以下两种作用域全局作用域在所有函数外部、最外层代码中声明的变量。这些变量在整个 .py 文件文件内都是可访问的局部作用域在函数内部声明的变量包括函数的形参。这些变量只能在当前函数体的内部被访问# 这是一个全局变量 global_var 我是全局变量 def demo_func(): # 这是一个局部变量 local_var 局部变量 print(global_var) # 正确函数内部可以安全读取全局变量 print(local_var) # 正确函数内部访问自己的局部变量 demo_func() print(global_var) # 正确全局区域访问全局变量 # print(local_var) # 错误引发 NameError: name local_var is not defined2. 作用域遮蔽与 global当全局变量与局部变量发生同名冲突时会触发特殊的遮蔽机制局部遮蔽现象如果在函数内部直接对一个与全局变量同名的变量进行赋值Python 会默认在当前函数内部创建一全新同名局部变量而不会去触碰外部的全局变量。这种现象被称为局部变量遮蔽了全局变量num 100 # 全局变量 def change_num(): num 200 # 隐式创建了一个同名的内部局部变量 num与外部的 num 互不干扰 print(f函数内部的 num: {num}) # 输出200 change_num() print(f外部全局的 num: {num}) # 输出100依旧是 100未被修改修改控制global关键字如果在某些特定业务场景下函数内部确实需要彻底改写外部全局变量的值则必须在函数体的第一行使用 global 关键字明确声明num 100 # 全局变量 def update_global_num(): global num # 显式声明接下来的 num 引用的是全局变量不要创建新的局部变量 num 500 # 修改全局变量的值 update_global_num() print(f修改后的全局 num: {num}) # 输出500全局变量已被成功改写注意在工程实践中应当极力避免在函数内部通过 global 修改全局变量。因为这会破坏函数的独立封装性让函数产生难以预料的副作用极大增加调试和维护的难度。如果需要改变数据首选通过 return 将新值投递出去交由主程序处理3. 变量的生命周期作用域决定了变量 在哪里可用而生命周期则决定了变量 在什么时候存活全局变量的生命周期从它被赋值的那一刻诞生直到整个程序执行结束或主进程被关闭时才会被销毁局部变量的生命周期具有瞬时性。它在函数被调用、执行流进入函数体的那一刻被动态创建在函数执行完毕、控制流跳出函数的那一刻立刻被销毁并释放内存。下一次重新调用该函数时会开启全新的生命周期五、嵌套与递归在掌握了函数的声明、数据交互与变量作用域后我们已经能够编写独立的模块。但在复杂的业务场景或特定的算法中函数还可以通过更高级的调用方式来实现更为强大的逻辑控制嵌套调用与递归调用1. 函数的嵌套调用函数的嵌套调用是指在一个函数的内部调用了另一个函数。这在模块化编程中是非常普遍的现象。当大函数执行时如果遇到了小函数的调用控制流会层层递进地跳转直到底层的小函数执行完毕并返回再依次退回外层函数def check_growth(age): # 子功能判断年龄段 if age 18: return 成年人 return 未成年人 def report(name, age): # 嵌套调用 check_growth 函数 identity check_growth(age) print(f用户报告 - 姓名{name}身份{identity}) # 启动主程序调用 report(小明, 20)执行流逻辑主程序调用 report - 控制流进入 report - 内部触发调用 check_growth - 控制流跳转进 check_growth- check_growth 执行完毕将结果 成年人 返回给 report - report 执行完剩余打印代码后返回主程序2. 递归调用递归调用是一种特殊的嵌套调用一个函数在其内部直接或间接地调用了它自身。递归在数学和算法领域拥有极高价值常常用于将一个庞大复杂的非线性问题层层拆解为规模较小、但形式完全相同的同类子问题递归模型的两大条件编写递归函数时必须严格遵守两个原则否则程序将陷入无法退出的死循环基线条件又称退出条件。即问题规模缩小到最小时可以直接得出答案、无需再次递归的终点递归步进每次自我调用时传入的参数规模必须比上一层更接近基线条件经典递归案例数学阶乘计算数学中 n 的阶乘记作 n!的定义为n! n * (n-1) * (n-2) * ... * 1用递推公式可以表达为当 n 1 时1! 1这就是基线条件def factorial(n): # 1. 基线条件到了最底层停止递归直接返回已知结果 if n 1: return 1 # 2. 递归步进n 乘以规模缩小了的 (n-1) 的阶乘 return n * factorial(n - 1) # 计算 5 的阶乘 (5 * 4 * 3 * 2 * 1) result factorial(5) print(f5的阶乘结果为{result}) # 输出1203. 递推与回归当执行 factorial(3) 时Python 内存的调用栈会经历两层完全相反的阶段递推阶段主程序唤醒 factorial(3)因为 3 ≠ 1代码走到 3 * factorial(2)此时 factorial(3) 的计算被挂起等待 factorial(2) 的答案程序开辟新内存启动 factorial(2)因为 $2 ≠ 1走到 2 * factorial(1)factorial(2) 被挂起程序启动 factorial(1)命中基线条件 n 1它立刻返回确定数值 1回归阶段4. 数值 1 被送回给 factorial(2)此时 factorial(2) 计算 2 * 1 得到 2 并返回5. 数值 2 被送回给 factorial(3)factorial(3) 计算 3 * 2 得到 6 并返回给主程序递归注意事项栈溢出每次执行未结束的递归调用操作系统都要在内存栈中保存当前函数的局部变量和运行状态。如果递归层次过深内存栈空间将被彻底耗尽Python 会强行抛出栈溢出异常。在 Python 中默认的最大递归深度通常限制在 1000 层左右综合案例简易计算器与菜单系统为了将前面内容融会贯通下面我们将设计并实现一个多功能终端计算器菜单系统在现代软件工程中我们推崇单职能原则主程序只负责业务调度与菜单展示具体的数学运算和功能逻辑则各自封装在独立的自定义函数中。这样做能让代码的结构更加清晰代码实现# 1. 功能函数封装层 (模块化单职能函数) def show_menu(): 打印控制台主菜单界面 print(\n 智能计算器系统 ) print( 1. 执行加法运算 (Addition)) print( 2. 执行减法运算 (Subtraction)) print( 3. 执行乘法运算 (Multiplication)) print( 4. 执行除法运算 (Division)) print( 0. 退出系统 (Exit)) print() def get_numbers(): 获取用户输入的两个数值。 Returns: float, float: 用户键入的两个操作数。 num1 float(input(请输入第一个数字)) num2 float(input(请输入第二个数字)) return num1, num2 def add(a, b): 计算加法 return a b def subtract(a, b): 计算减法 return a - b def multiply(a, b): 计算乘法 return a * b def divide(a, b, /): 计算除法使用强制位置参数 / 保证接口严谨 if b 0: return 错误除数不能为零。 return a / b # 2. 业务调度控制层 (主程序循环) def main(): 系统主控制逻辑 while True: # 调用函数展示菜单 show_menu() # 接收用户选择 choice input(请选择您要执行的操作 (0-4)) # 流程控制退出系统 if choice 0: print(感谢使用本系统再见) break # 状态拦截如果输入非法选项直接跳过本轮循环重新提示 if choice not in [1, 2, 3, 4]: print(输入不合法请输入 0 到 4 之间的数字) continue # 调用数据获取函数实现函数嵌套调用 n1, n2 get_numbers() # 多分支结构匹配具体计算逻辑 if choice 1: result add(n1, n2) print(f计算结果{n1} {n2} {result}) elif choice 2: result subtract(n1, n2) print(f计算结果{n1} - {n2} {result}) elif choice 3: result multiply(n1, n2) print(f计算结果{n1} * {n2} {result}) elif choice 4: result divide(n1, n2) print(f计算结果{n1} / {n2} {result}) # 3. 程序启动 main()综合分析高内聚与低耦合数学运算函数非常简单。它们不需要关心数据是从网络来、还是从控制台键盘来只需要接收数据通过返回值输出数据。这种高内聚的设计使它们随时可以被移植到其他项目中复用函数嵌套调用在主控制函数 main() 中我们高频调用了 show_menu()、get_numbers() 以及各计算函数。控制流在主程序与各函数之间清晰地跳转健壮性容错在 divide 函数内部我们拦截了数学中 除数为0 的非法物理状态并通过返回错误文本的方式规避风险总结在本篇教程中我们完成了向模块化编程的跨越我们首先明确了函数在消除代码冗余、提升可复用性方面的核心工程价值。随后我们解构了自定义函数的语法模型梳理了程序执行时控制流在调用栈中的跳转细节。在数据交互层面我们明晰了形参与实参的区别掌握了位置参数、关键字参数、缺省参数的混合运行规则并补充了应对不确定边界的可变参数*args、kwargs以及控制接口严谨性的限制参数符号/ 与 *此外我们讨论了工业标准的Docstring 规范厘清了全局与局部作用域的变量遮蔽和生命周期并在最后探究了函数递归调用机制。通过最后的菜单控制系统我们真正见证了模块化分工的魅力截止到目前我们处理的所有数据都还停留在单一的数字、字符串或布尔值上。当面对海量复杂的成组数据时程序该如何高效组织在下一篇教程中我们将正式推开 Python 进阶的大门深度剖析高级组合数据类型列表与元组的存储与管理艺术