资讯动态

Python第五章函数与模块化课后练习精讲:返回值、参数传递与作用域避坑指南

发布时间:2026/10/17 11:21:15 来源:尧图企业网站定制
1. 为什么第五章是Python学习的分水岭1.1 从“能跑就行”到“结构思维”的转折点翻到《Python程序设计》第五章的时候很多人会突然感觉难度上了一个台阶。前面几章还在跟变量、循环、字符串打交道到了第五章函数、模块化、参数传递、作用域这些概念一股脑涌上来代码量也从十几行的小脚本变成了几十行甚至上百行的结构化程序。我带过不少初学Python的朋友他们普遍反馈前四章的练习还能靠直觉硬写第五章开始就必须真正理解“程序是怎么组织的”了。这一章的核心其实就一件事——把代码拆成可复用、可维护的单元。听起来简单但真正动手做课后练习的时候你会发现坑特别多。比如函数返回值搞不清楚什么时候返回None参数传递时可变对象和不可变对象的行为差异局部变量和全局变量同名时的覆盖问题模块导入时的路径和命名冲突……这些都不是看一遍书就能绕过去的。我写这篇东西的目的很直接把第五章课后练习里那些容易卡住人的地方一个一个拆开讲清楚。不是简单贴个答案就完事而是把每道题背后的设计意图、常见错误写法、调试思路都摊开来说。适合正在自学这本书的读者也适合拿这本书当教材的辅导者参考。你不需要先看完前四章才能看这篇但至少得写过一些Python代码知道def是干什么的。1.2 第五章练习的典型题型分布从历年学习者反馈来看第五章的课后练习大致可以分成四类函数定义与调用类、参数传递与返回值类、作用域与命名空间类、模块化与代码组织类。这四类不是孤立的很多题目会交叉出现。比如一道题可能既要求你写一个函数处理字符串又要求你把函数放到模块里再在主程序中导入调用。我统计过某学习社区里关于这一章的高频提问排在前三的分别是“为什么我的函数没有返回值”、“函数里修改了列表但外面没变”、“导入模块时报ModuleNotFoundError”。这三个问题恰好对应了返回值机制、可变对象引用传递、模块搜索路径这三个核心知识点。后面的章节我会逐一展开先把整体框架理清楚。提示做第五章练习时建议每道题都先手写伪代码把输入、处理、输出三个阶段标清楚再动手敲Python。这个习惯能帮你减少一半以上的逻辑错误。2. 函数定义与返回值那些让你反复调试的细节2.1 函数签名的设计逻辑第五章的练习里很多题目会给出函数的功能描述但不会告诉你参数该怎么设计。比如“写一个函数判断一个数是否为素数”有人写成def is_prime(n)有人写成def is_prime()然后在函数内部用input()获取输入。两种写法都能跑但前者是正确方向后者把输入输出和逻辑处理混在一起了。为什么强调函数签名因为函数的核心价值是封装可复用的逻辑。如果函数内部直接跟用户交互那这个函数就没法在别的程序里复用。我见过不少初学者图省事所有函数都写成无参数的然后在函数体里input()和print()结果到了后面需要批量处理数据时代码完全没法改。正确的做法是参数负责接收数据返回值负责输出结果输入输出放在主程序里。这个原则在第五章的练习中反复出现但很多人第一次学的时候意识不到。举个例子练习里有一道题要求“写一个函数计算阶乘”标准写法应该是def factorial(n): if n 0: return None result 1 for i in range(2, n 1): result * i return result然后在主程序里调用factorial(5)并打印结果。而不是在函数里直接print(result)。这个区别看起来小但决定了你的函数能不能被测试、能不能被复用。2.2 返回值的隐式陷阱Python函数有一个很容易让人踩坑的特性没有显式return语句时函数返回None。这个规则本身简单但在练习中经常以隐蔽的方式出现。比如下面这段代码def add_to_list(item, target_list): target_list.append(item)调用result add_to_list(3, [1, 2])之后result是None但[1, 2]变成了[1, 2, 3]。很多初学者会困惑“我明明没写return为什么列表变了”这就是可变对象引用传递和返回值机制交织在一起造成的困惑。第五章练习里有一类题目专门考察这个点要求写一个函数修改传入的列表然后返回修改后的列表。如果你只写了target_list.append(item)而没有return target_list调用方拿到的就是None。正确的写法是def add_to_list(item, target_list): target_list.append(item) return target_list或者更稳妥的做法是不修改原列表而是创建新列表返回def add_to_list(item, target_list): new_list target_list.copy() new_list.append(item) return new_list两种写法没有绝对的对错取决于题目要求。但你必须清楚自己在做什么以及调用方会拿到什么。2.3 多返回值的拆包技巧第五章有些练习要求函数返回多个值比如“写一个函数同时返回一个数的平方和立方”。Python支持直接返回元组def square_and_cube(n): return n ** 2, n ** 3调用时可以用拆包sq, cb square_and_cube(3)。这个语法很简洁但初学者容易在调试时犯迷糊。比如result square_and_cube(3)result是一个元组(9, 27)如果你直接print(result)看到的是(9, 27)但如果你写print(result[0])才能拿到9。我建议在做这类练习时先在交互式环境里试一下返回值到底是什么类型再决定怎么用。另外如果返回的值超过三个考虑用字典或者命名元组可读性会更好。比如def analyze_number(n): return {square: n ** 2, cube: n ** 3, is_even: n % 2 0}这样调用方通过键名访问不容易搞混顺序。注意返回多个值时Python实际上是把它们打包成一个元组。如果你在函数里写了return a, b调用方拿到的是一个元组不是两个独立的变量。拆包是调用方的操作不是函数本身的行为。3. 参数传递机制可变与不可变的分水岭3.1 传对象引用到底意味着什么Python的参数传递既不是纯值传递也不是纯引用传递官方说法是“传对象引用”。这个概念在第五章练习里体现得特别明显因为很多题目会要求你写函数处理列表、字典这类可变对象。用一个生活化的类比假设你有一张纸条上面写着某个储物柜的编号。你把纸条复印一份给朋友朋友拿着复印件去储物柜里放东西或取东西。纸条本身是复印件参数是引用的副本但储物柜是同一个对象本身。如果朋友往储物柜里放东西你去看的时候也能看到但如果朋友把复印件上的编号改成另一个储物柜你手里的纸条不受影响。对应到Pythondef modify_list(lst): lst.append(4) # 修改了原列表 def reassign_list(lst): lst [4, 5, 6] # 只是让局部变量指向新列表 original [1, 2, 3] modify_list(original) print(original) # [1, 2, 3, 4] reassign_list(original) print(original) # [1, 2, 3, 4]没有变成[4, 5, 6]这个区别是第五章练习里最容易出错的地方之一。很多题目要求“写一个函数在列表末尾添加元素”如果你写成lst lst [item]原列表不会变必须用lst.append(item)或者lst [item]。3.2 默认参数的坑与正确用法第五章有些练习会涉及默认参数比如“写一个函数可以指定问候语默认是Hello”。默认参数本身不难但有一个经典陷阱默认参数在函数定义时只计算一次。如果默认值是可变对象就会出现意想不到的共享行为。def add_item(item, target[]): target.append(item) return target print(add_item(1)) # [1] print(add_item(2)) # [1, 2]而不是[2]这是因为默认的[]在函数定义时就创建了之后每次调用都共用同一个列表。正确的做法是用None作为默认值在函数内部判断def add_item(item, targetNone): if target is None: target [] target.append(item) return target这个模式在第五章的练习中会反复用到尤其是那些要求“可选参数”的题目。我建议你把这个写法记牢以后写任何带默认参数的函数都先想一想默认值是不是可变的。3.3 关键字参数与位置参数的混用规则第五章练习里有些函数参数比较多比如“写一个函数接收姓名、年龄、城市返回格式化字符串”。调用时如果全用位置参数很容易搞混顺序def format_info(name, age, city): return f{name}, {age}, {city} format_info(25, 张三, 北京) # 顺序错了也能跑但结果不对用关键字参数就清晰多了format_info(name张三, age25, city北京)Python的规则是位置参数必须在关键字参数前面。也就是说format_info(张三, age25, city北京)是合法的但format_info(name张三, 25, 北京)会报语法错误。这个规则在练习中经常被忽略导致SyntaxError。另外如果你在设计函数时希望某些参数只能通过关键字传递可以用*分隔def format_info(name, *, age, city): return f{name}, {age}, {city}这样调用时必须写format_info(张三, age25, city北京)不能省略参数名。这个技巧在第五章的进阶练习里会出现属于加分项。4. 作用域与命名空间变量到底在哪里生效4.1 局部变量、全局变量与LEGB规则第五章练习里有一类题目专门考察变量作用域比如“在函数内部修改全局变量会怎样”。Python的作用域规则可以用LEGB来概括Local局部、Enclosing闭包、Global全局、Built-in内置。查找变量时按这个顺序逐层向上找。count 0 def increment(): count count 1 # 这里会报UnboundLocalError这段代码会报错因为函数内部对count赋值时Python把它当作局部变量但赋值前又引用了它所以报“局部变量在赋值前被引用”。正确的做法是用global声明count 0 def increment(): global count count count 1但我要提醒一句第五章练习里凡是要求用global的题目都是在教你理解作用域而不是鼓励你大量使用全局变量。实际项目中全局变量越少越好因为难以追踪和测试。更好的做法是把状态封装在类里或者通过参数和返回值传递。4.2 嵌套函数与nonlocal关键字第五章的进阶练习里会出现嵌套函数也就是在一个函数内部定义另一个函数。这时候如果内层函数要修改外层函数的变量需要用nonlocaldef outer(): x 10 def inner(): nonlocal x x 20 inner() return x # 返回20nonlocal和global的区别在于global指向模块级别的全局变量nonlocal指向最近的外层函数变量。这个知识点在练习中通常以“写一个计数器函数”的形式出现要求每次调用返回递增的值。用闭包实现是这样的def make_counter(): count 0 def counter(): nonlocal count count 1 return count return counter c make_counter() print(c()) # 1 print(c()) # 2这个模式在第五章的练习里属于比较有难度的题目但理解了作用域规则之后就不难写。4.3 变量遮蔽与命名冲突作用域还有一个常见问题是变量遮蔽内层变量和外层变量同名时内层会覆盖外层。比如x 100 def func(): x 200 print(x) # 200 func() print(x) # 100函数内部的x是局部变量不会影响全局的x。但如果函数内部没有赋值只是读取就会用到全局的xx 100 def func(): print(x) # 100 func()这个规则看起来简单但在练习中经常以隐蔽的方式出现。比如你在函数内部写了一个循环变量i而全局也有一个i函数执行完后全局的i不会变。但如果你在函数内部用了global i那就会改掉全局的i。我建议在做第五章练习时给局部变量起名尽量具体避免用i、x、temp这种容易冲突的名字。提示如果你在函数里遇到UnboundLocalError先检查是不是对某个变量赋值了但又在赋值前引用了它。解决办法要么用global/nonlocal声明要么换个变量名。5. 模块化与代码组织从单文件到多文件5.1 模块导入的三种写法与适用场景第五章后半部分开始涉及模块化练习里会要求你把函数放到单独的.py文件里然后在主程序中导入。Python有三种常见的导入方式import mymodule mymodule.my_function() from mymodule import my_function my_function() from mymodule import * my_function() # 不推荐第一种最安全命名空间清晰第二种适合只导入少量函数第三种会污染当前命名空间容易造成命名冲突实际项目中应该避免。第五章练习里通常要求用第一种或第二种具体看题目说明。我个人的习惯是如果模块里函数很多用import mymodule然后通过mymodule.xxx调用如果只用一个函数用from mymodule import xxx。这样代码可读性最好别人一看就知道函数是从哪里来的。5.2__name__ __main__的真正含义第五章练习里有一道经典题目写一个模块既能被导入使用又能直接运行测试。这时候就需要用到def main(): # 测试代码 pass if __name__ __main__: main()这个写法的含义是当文件被直接运行时__name__的值是__main__所以会执行main()当文件被导入时__name__是模块名不会执行main()。很多初学者不理解为什么要加这个判断觉得直接写测试代码也能跑。区别在于如果你不加这个判断别人导入你的模块时测试代码也会被执行可能会打印一堆无关信息或者产生副作用。我建议从第五章开始养成给每个模块加if __name__ __main__:的习惯。这样你的代码既能当脚本跑又能当模块导入灵活性大大提升。5.3 包结构与相对导入的注意事项如果练习要求你把多个模块组织成包就需要创建文件夹和__init__.py文件。比如mypackage/ __init__.py module_a.py module_b.py然后在module_b.py里导入module_afrom . import module_a这里的.表示当前包。相对导入只能在包内部使用不能在顶层脚本里用。第五章的练习通常不会涉及太复杂的包结构但了解这个机制对后续学习有帮助。常见错误是直接运行包内的模块文件导致相对导入报错。解决办法是用python -m mypackage.module_b的方式运行或者把导入改成绝对导入。这个坑我在第一次学的时候踩过调试了半天才搞明白。6. 常见问题与排查技巧实录6.1 函数相关错误速查表错误现象可能原因排查方法NameError: name xxx is not defined函数名拼写错误或未定义检查函数定义和调用的拼写是否一致TypeError: xxx() missing 1 required positional argument调用时少传了参数检查函数签名和调用处的参数个数UnboundLocalError函数内对变量赋值前引用了它检查是否需要global/nonlocal或换变量名函数返回None没有显式return语句检查函数体最后是否有return修改列表后原列表没变用了lst lst [item]而非lst.append(item)确认是原地修改还是重新赋值ModuleNotFoundError模块文件不在搜索路径中检查文件位置和当前工作目录ImportError: cannot import name xxx模块中没有该名称检查模块内函数名拼写和导入语句这张表覆盖了第五章练习中80%以上的报错场景。遇到问题时先对照表格排查能省不少时间。6.2 调试函数的三步法我在做第五章练习时总结了一个调试函数的三步法分享给你第一步单独测试函数。不要一上来就跑整个程序先把函数拿出来用几个简单的输入测试它的输出。比如你写了一个排序函数先用[3, 1, 2]测试再用[]和[1]测试边界情况。第二步打印中间变量。在函数内部关键位置加print语句看看变量的值是否符合预期。虽然有人觉得print调试很原始但在学习阶段这是最直接有效的方法。第三步检查返回值类型。用type()函数确认返回值是你期望的类型。比如你期望返回列表结果返回了元组后面的代码就会出错。这三步看起来简单但能帮你快速定位大部分问题。我见过很多初学者一遇到报错就到处改代码结果越改越乱。按步骤来效率高得多。6.3 几个容易被忽略的细节第一个细节函数默认参数不要用可变对象。前面已经讲过这里再强调一次因为这是第五章练习里最高频的坑。第二个细节函数名不要和内置函数重名。比如你写了一个def sum(lst)后面想用内置的sum()就用不了了。常见被覆盖的内置函数名有sum、max、min、list、dict、input、print。起名之前先想一想是不是内置函数。第三个细节模块文件名不要和标准库重名。比如你创建了一个random.py然后import random导入的是你自己的文件而不是标准库会导致奇怪的问题。这个坑很隐蔽因为程序可能不报错但行为完全不对。第四个细节函数文档字符串。第五章练习里有些题目要求给函数写文档字符串格式是函数定义下方用三引号包裹的字符串。虽然不写也能跑但写了之后可以用help()查看也方便别人理解你的函数。我建议从第五章开始养成写文档字符串的习惯。注意如果你在导入模块时遇到问题先检查当前工作目录下有没有和模块同名的文件。Python会优先导入当前目录下的文件这经常导致意外的导入结果。7. 从练习到实战的思维转变7.1 练习答案不是终点很多人做完第五章练习就对答案对了就过错了改一改也过。但我想说的是课后练习的价值不在于答案本身而在于你写代码时的思考过程。同一道题有人写了十行有人写了三行都能得到正确结果但代码质量差别很大。比如“写一个函数判断素数”这道题最直观的写法是用循环从2到n-1逐个试除。但如果你稍微想一下只需要试到平方根就够了因为如果n有大于平方根的因子必然有小于平方根的对应因子。这个优化不需要题目要求但体现了你对问题的理解深度。我建议每做完一道题问自己三个问题这个函数能不能更简洁边界情况处理了吗如果输入是空列表或负数会怎样这三个问题能帮你从“能跑就行”提升到“写得靠谱”。7.2 把练习代码改造成可复用模块第五章学完之后你可以尝试把之前写的练习函数整理成一个工具模块。比如把所有跟数字处理相关的函数放到number_utils.py所有跟字符串处理相关的放到string_utils.py。然后写一个主程序导入这些模块测试它们的功能。这个过程能帮你巩固模块化的知识也能让你体会到“代码复用”的实际好处。以后遇到类似问题时直接导入自己的模块就行不用重新写一遍。我在学习阶段整理过一个自己的工具模块后来在很多小项目里都直接用省了不少时间。7.3 下一步该学什么第五章的函数和模块化是Python编程的基础设施后面的类与对象、文件操作、异常处理都会用到这些知识。如果你第五章的练习做得比较顺可以提前看一下第六章的类与对象理解一下如何把函数和数据封装在一起。如果你觉得第五章还有些模糊建议把作用域和参数传递这两块再写几个小例子练一练这两块是后面所有内容的地基。我在实际教学中发现第五章学得扎实的人后面学类和装饰器时明显轻松很多。因为类的方法本质上就是函数装饰器本质上就是接收函数返回函数的高阶函数。所以花时间把第五章吃透绝对是划算的。最后分享一个小技巧做练习时不要只看题目要求试着改一改题目条件看看代码还能不能跑。比如题目要求处理正整数你试试输入0或负数会怎样题目要求处理非空列表你试试空列表会怎样。这种“折腾”能帮你发现很多隐藏的bug也能让你对Python的行为理解得更透彻。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑