最近整理自己的学习笔记,翻出一个标着py03的文件夹,里面是函数、模块、作用域相关的练习记录。如果你也在学Python,或者正在某套课程里按编号往下走,看到py03应该不陌生:这是很多系列课程的第三阶段,py01通常是环境搭建和基础语法,py02是数据类型和流程控制,到py03就切到了函数与模块化编程。这个阶段要解决的核心问题,不是再记几个语法点,而是开始学习“怎么组织代码”。这篇笔记就围绕py03阶段展开,内容包括课程设计思路、函数参数的坑、作用域规则、模块导入的常用姿势,以及我写成绩统计工具时踩过的报错和排查方法。适合刚啃完循环和列表、准备往进阶走的朋友,也适合正在补函数这块知识缺口的学习者。
1. py03这个阶段到底在训练什么
1.1 课程编号里的隐性路径:从语法到工程思维
先说课程编号。我在不少Python课里看到的套路是py01/py02/py03这样的序号,含义大同小异:py01解决环境问题和基础输出,py02把列表、字典、元组、条件分支、for/while循环这些“建筑材料”过一遍,py03则开始讲函数、模块、作用域和异常处理。这其实是一条很清楚的成长路径——先有砖块,再学砌墙,最后才是盖房子。py03正好处在“砌墙”的位置。
你发现没有,到了py03,学习难度突然上了一个台阶。不是因为函数本身难,而是课程开始要求你同时控制多个变量、多个函数之间的数据传递,甚至是多个文件。很多人在前两个阶段跟着教程抄代码觉得很顺,到函数这块突然卡住,原因就在这里:学习目标变了,从“把语法写对”变成了“把逻辑设计对”。
这个阶段如果你能迈过去,后面学面向对象、学网络请求、学Web框架都会轻松很多。因为那些内容本质上都是在更高维度上做“组织代码”这件事。函数的调用关系、参数的传值时机、模块之间的依赖,这些基本功不扎实,后面排错会非常痛苦。所以千万别觉得py03不过就是学个def,这一章的厚度其实远超字面意思。
1.2 为什么函数和模块总是被放在同一个阶段
这是我复盘课程设计时想明白的一个点。函数解决的是“对一段逻辑的复用”,模块解决的是“对一组逻辑的复用”。两者天然是连续的:你先把一个功能写进函数,再把几个相关的函数放进同一个.py文件,这个文件就成了一个模块。所以课程把这两块内容放在同一个编号阶段,逻辑上是顺的。
放在一起还有一个好处:可以尽早培养“按职责拆代码”的习惯。我见过不少自学了很久的人,所有代码都堆在一个脚本里,从头到尾几百行,中间反复用Ctrl+F找变量,改一处错了三处。这根本不是编码能力的问题,是缺少模块化训练。py03阶段的函数加模块组合,其实就是在强制你养成拆分的习惯:先定义一个函数处理单一任务,再通过import把它组装进更大的程序。
如果你是在自学而不是上课,我强烈建议别跳着学。哪怕你已经会用def写个简单函数,也把py03阶段的练习完整过一遍。这个阶段看似简单,但里面埋着默认参数、作用域、命名空间这些细节,跳过的话后面会在莫名其妙的地方翻车。我见过太多人学到装饰器时彻底蒙圈,回头一查,根子就是函数作用域没吃透。
2. 核心知识点:函数参数、作用域与模块导入
2.1 函数参数的五种形态,以及最容易踩的默认参数坑
先说说参数的形态。基础阶段你会接触到位置参数,就是按顺序传进去的那种:
def greet(name, message): return f"{name}, {message}"然后是默认参数,调用时可以省略:
def greet(name, message="你好"): return f"{name}, {message}"这里有一个经典的大坑:默认值不要用可变对象。举个例子:
def add_item(item, container=[]): container.append(item) return container第一次调用add_item("a")看起来很正常,返回["a"]。第二次再调用add_item("b"),结果变成了["a", "b"],而不是["b"]。原因是在Python里,默认参数在函数定义的时候就被求值并且只创建一次,后续调用共享的是同一个列表对象。正确的写法是:
def add_item(item, container=None): if container is None: container = [] container.append(item) return container这种“默认参数陷阱”是py03阶段几乎必考的一个细节,值得专门记一笔。顺带说一句,如果你想让多个调用方各自维护独立的容器,这个写法就是标准答案,千万别嫌多两行代码。
接着是*args和**kwargs。它们允许函数接收任意数量的位置参数或关键字参数,常用于写装饰器、封装接口或者做参数透传。核心规则是:args是元组,kwargs是字典,且位置参数必须放在它们前面:
def log(level, *args, **kwargs): print(level, args, kwargs) log("INFO", 1, 2, 3, user="tom")输出会是INFO (1, 2, 3) {'user': 'tom'}。你需要注意,*args和**kwargs只是约定俗成的名字,真正起作用的星星号语法。Python 3 还支持仅限关键字参数,写法是在*args后面直接写参数名,比如def func(a, *, b):,这样b只能以关键字形式传参,能有效防止调用顺序错乱。
还有一个容易被忽略的小知识:函数return可以返回多个值,实际返回的是一个元组。比如def get_min_max(nums): return min(nums), max(nums),调用后用min_value, max_value = get_min_max(scores)做解包,这是py03阶段很常用的操作。记住这个细节,后面学列表推导、学解包语法时会顺畅很多。
2.2 作用域与命名空间:LEGB规则到底在说什么
作用域是py03阶段最容易让人头晕的部分。Python查找一个名字的时候,遵循的是LEGB顺序:Local(当前函数内部的局部作用域)、Enclosing(外层嵌套函数的作用域)、Global(模块级别的全局作用域)、Built-in(Python内置名字)。这个顺序决定了同名变量谁会生效。
你可以把它理解成找东西的流程:先在当前房间找,找不到就打开门去外面客厅找,再找不到就去小区门口问保安,最后才去翻城市公共目录。公共目录就是Built-in那一层。Python内置的len、print都在这一层,所以你能直接调用。
一个很常见的问题是,在函数内部给全局变量赋值,看上去没报错,但函数执行完,全局变量还是原来的值。比如:
count = 0 def increase(): count = 1 increase() print(count) # 输出 0原因在于函数内部的count = 1创建了一个新的局部变量,和全局的count不是同一个。如果你确实想修改全局变量,必须显式声明global count。py03的练习里很多同学在这里翻车,最后发现是对名字查找规则理解不到位。
更进阶的是nonlocal,它作用在外层嵌套函数的局部作用域里。典型场景是闭包里的计数器:
def make_counter(): times = 0 def count(): nonlocal times times += 1 return times return count如果不写nonlocal,内层函数对times的赋值同样会被当成局部变量,运行时直接抛UnboundLocalError。这个报错很经典,后面我会专门讲排查方法。理解了LEGB,你基本就可以预判绝大多数“变量不按预期变化”的问题了。顺便提一句,闭包这个概念在py03只做了解性要求,但你把nonlocal玩明白了,后面写装饰器会特别顺手。
2.3 模块导入的姿势:import、from import 与name机制
模块化编程的第一步是把函数存进一个.py文件,然后在另一个文件里导入。最常见的三种写法:
import statistics # 引入整个模块,用 statistics.mean(...) 访问 from statistics import mean # 只引入名字,直接用 mean(...) import statistics as st # 起别名,减少输入三者没有绝对优劣,但要遵守一条原则:能限定就限定。如果只用到一两个函数,from import更清晰;如果模块名太长或者容易冲突,as别名更顺手。无脑import整个模块的问题在于,一旦项目变大,命名空间里全是模块名,反而看不清代码逻辑。
我建议在py03阶段就养成在脚本里加__name__守卫的习惯:
def main(): # 主逻辑 pass if __name__ == "__main__": main()当你直接运行这个脚本时,__name__的值是"__main__",main()会执行;当它被其他脚本import的时候,__name__是模块名,main()不会自动执行。这个机制防止了“导入模块时误触发一堆代码”的问题,是模块化编程的第一道保险。你可以在py03阶段就要求自己:任何一个独立可运行脚本都写上这个守卫,养成肌肉记忆。
还有一个容易忽略的点:不要循环导入。也就是a.py导入了b.py,b.py又回头导入a.py。一旦出现这种结构,Python在加载过程中会因为名字还没定义而报ImportError。解决办法是重新设计模块依赖,让依赖方向变成单向的,或者把相互引用的代码移到第三个模块里。另外,如果你遇到ModuleNotFoundError: No module named 'xxx',可以先检查三件事:模块文件所在目录是否在sys.path里、文件名是否拼错、以及模块名是否和标准库重名。py03阶段最常踩的就是自己写了random.py,然后在另外的脚本里import random,结果导入的根本不是标准库那个random。
3. 实操过程:用一套练习把知识点串起来
3.1 写一个成绩统计模块,从函数到模块完整走一遍
这里我用自己的笔记记录来演示一个可复现的练习。目标是做一个成绩统计小工具,输入一组分数,输出平均分、最高分、最低分,并且支持按条件筛选。第一步,新建一个stats.py,先写一个安全的平均值函数:
def mean(nums): if not nums: raise ValueError("分数列表不能为空") return sum(nums) / len(nums)注意我在函数入口加了空列表检查,这是py03阶段该有的意识:函数不能只对“正常输入”负责,还要考虑异常情况。第二步,写筛选函数,比如过滤掉低于60分的成绩,并计算剩余成绩的平均分:
def mean_above(nums, threshold=60): filtered = [n for n in nums if n >= threshold] return mean(filtered)这里顺便就用上了默认参数。第三步,加一个排序需求,用lambda表达式按自定义规则排序。假设分数存放在字典列表里,每条记录是{"name": "张三", "score": 88},要求按成绩降序输出:
def sort_by_score(records): return sorted(records, key=lambda r: r["score"], reverse=True)lambda在这里的好处是不用单独def一个命名函数,逻辑短小的时候可读性反而更高。第四步,把前面这些函数集中到一个模块里,在另一个脚本里导入使用。调用方只需要:
from stats import mean, mean_above, sort_by_score scores = [55, 78, 90, 63, 42] print(mean(scores)) # 65.6 print(mean_above(scores)) # 77.0 records = [{"name": "张三", "score": 88}, {"name": "李四", "score": 61}] print(sort_by_score(records))输出会是[{'name': '张三', 'score': 88}, {'name': '李四', 'score': 61}],排序生效。这个练习的妙处在于,它把函数定义、参数默认值、列表推导、lambda、模块导入全部串在了同一个真实场景里。你写一遍之后,对py03阶段的知识点会有一个整体感受:原来每个零散概念在项目里是这样配合工作的。我甚至可以再加一句,这个成绩模块在后面学文件读写、学Pandas时还能继续扩展,前期实现得越规范,后面挂新功能越省事。
3.2 真实脚本里怎么组织模块结构
练习完上面的小模块,再进一步看看真实项目的组织方式。我习惯把代码拆成三层:输入层、计算层、输出层。计算层就是各种纯粹的运算函数,输入层负责获取数据,输出层负责展示结果。这样做的好处是,以后想换数据来源,或者改成命令行输出、文件输出,都只动对应层,不会牵连计算逻辑。说白了,就是把“做什么”和“怎么做”分开,把“数据怎么来”和“数据怎么用”分开。
一个常见的初学者习惯是,把所有功能全部写在main()函数里,几十个操作顺序执行,中间穿插print调试。这在练习阶段可以,但程序一旦超过一两百行,改起来就非常痛苦。更好的做法是main()只负责编排:
def main(): raw_data = load_data() result = analyze(raw_data) show_result(result) if __name__ == "__main__": main()每个步骤对应的函数单独放在自己的区域或模块里,职责清晰。你在py03阶段就可以按这个思路去练,哪怕只是几十行的小脚本,也用这种“入口-处理-输出”的结构去搭。我以前写练习代码时喜欢全都堆在一个文件里,觉得拆开麻烦,后来有一次需要复用统计函数,发现要从几百行里把函数抠出来特别费劲,从那以后就老老实实拆文件了。养成习惯之后,再去学类、学框架,会觉得代码的组织是水到渠成的。
4. 常见问题与排查技巧实录
4.1 初学函数阶段最常踩的四个报错
我整理了一份py03阶段出现频率最高的报错对照表,基本都是我当年记在笔记里的真实案例:
| 报错信息 | 触发场景 | 解决思路 |
|---|---|---|
NameError: name 'xxx' is not defined | 拼写错误,或变量在函数外定义 | 检查名字拼写,确认变量是否在可访问的作用域内 |
TypeError: missing 1 required positional argument | 调用函数时少传了参数 | 检查函数定义,确认每个必填参数都传了值 |
UnboundLocalError | 函数内修改外层变量却没声明nonlocal/global | 按LEGB分析名字归属,补上global或nonlocal声明 |
ImportError/ModuleNotFoundError | 模块名写错、未安装、循环导入 | 检查模块路径、第三方库是否安装、拆解依赖关系 |
每个报错背后都有“为什么”。比如UnboundLocalError,根因是Python在编译函数时发现你对某个名字做了赋值,于是把它标记为局部变量,后续访问同一函数里的这个名字都会走局部作用域,哪怕赋值语句在访问之后才出现。理解了这一点,你看到UnboundLocalError时就不会瞎试,而是直接去分析变量名归属。
排查顺序我建议固定下来:先看报错信息最后一行,确定是什么类型的错误;再往上看代码位置;最后结合作用域规则判断。不要一上来就在网上搜整段代码,搜一次两次还行,每次都搜就没进步了。实际操作中,把报错信息完整复制下来,先自己读一遍,很多时候问题就出在你以为“Python在A处报错,所以是A的问题”,其实真正的错误在B处,只是到A才暴露。
4.2 我自己的三个学习习惯,专治“学完就忘”
第一,每个知识点配一个最小可运行脚本。学默认参数那天,我就只写一个几行的文件,故意踩一次可变默认参数的坑,然后把正确写法贴在注释里。这个脚本文件名带日期和主题,比如2025-05-06_default_arg.py。两个月后复习,直接看这些带现场记录的小文件,比看那种几百页的笔记效率高得多。
第二,用调试器代替print。很多人习惯到处print看值,打印完还要记得删。我的做法是直接在IDE里打断点,看变量在每一步的值。py03阶段练习函数时,断点尤其好用:你能清楚看到参数传入前后、return语句执行时变量是什么状态。这个习惯越早建立越好,后面学递归、学闭包时,断点就是你的眼睛。
第三,给函数写“测试脚本”。不用学什么测试框架,就定义一个main,里面放几组输入,打印结果,手动比对预期值。像上面那个成绩统计模块,我会额外测空列表会怎么处理、只有一个分数时正不正常。这个动作能逼着你把边界条件想清楚,而边界条件恰恰是新手代码最容易崩的地方。我自己到现在写工具函数,依然保留这个“随手补几行测试”的习惯,省下来的排错时间远比写测试的时间多。
最后再分享一个我个人的体会。py03这个阶段,最大的变化不是学会了def这个关键字,而是开始有了“我在设计代码”的感觉。函数是抽象的边界,模块是组织的边界,当你第一次把一个功能封装进函数、再用import把它组装到另一个程序里时,那种“代码可以被复用”的感觉是很妙的。建议你也把这段时间的练习脚本都留好,命名规范一点,两个月后回头看,你会看到自己在代码组织能力上非常明显的进步。