1. 函数的本质把积木拼成机器先说个场景。你刚学完变量和循环写了这段代码score1 85 if score1 60: print(及格) else: print(不及格) score2 42 if score2 60: print(及格) else: print(不及格)写两个判断还好如果写十个、一百个呢键盘盲打速度再快这种复制粘贴也难免出问题——漏改一个变量名就是Bug。更麻烦的是某天产品经理过来跟你说“及格线改成70分”。你得从头到尾把所有if重新翻一遍漏改一处就等着背锅吧。这正是函数的诞生动机。函数 一段可以被反复调用的命名代码块它把“判断是否及格”这个逻辑封装成积木每块积木做好接口参数和返回值剩下的就是拼装。Python里万物皆对象函数也不例外——它能被赋值给变量、塞进列表、传进另一个函数这些高阶玩法以后会碰到但今天先把地基打好。这堂课的内容是Python语法第三课我会从零开始讲清楚四件事如何定义一个函数def语句、函数名规则、缩进陷阱参数到底有哪些玩法位置参数、默认参数、*args、**kwargs返回值是什么return的机制、多值返回、隐式None作用域怎么影响你的变量局部变量、全局变量、global、nonlocal每部分都会配上直接能跑的代码和解释为什么这么做不整虚的。2. 函数的定义与调用先学走再学跑2.1 def语句的完整语法定义一个函数Python的语法非常简洁只需要def关键字def greet(name): 接收一个名字打印问候语 print(f你好{name})拆开看一个函数定义由四个部分组成def关键字告诉Python“我要定义一个函数了”函数名遵循变量命名规则只允许字母、数字、下划线且不能以数字开头。建议用下划线分隔多个单词snake_case比如calculate_average_score别用calculateAverageScore那种驼峰风格会被人嫌弃参数列表写在圆括号里多个参数用逗号分隔。参数可以没有但不能省略圆括号冒号和缩进体冒号表示函数体的开始下面的缩进代码块就是函数的具体逻辑。缩进必须一致同一个函数内用4个空格千万别混Tab这里还有一行接收一个名字打印问候语这叫文档字符串docstring。它不是注释而是函数的元数据可以通过greet.__doc__访问很多工具能自动提取它生成API文档。养成写docstring的习惯三个月后回头看自己的代码会感谢当时的手。调用函数的方式也很直接函数名加上一对括号greet(张三) greet(李四) greet(王五)输出三行问候语。每当执行到greet(...)Python就跳进函数体内跑完再回到原来的位置继续往下走。这个“跳进去再跳回来”的过程叫函数调用栈理解它有助于理解后面的作用域和递归。2.2 函数定义放在哪里Python是一门解释型语言代码从上到下执行。所以函数必须先定义后调用。你把调用写在定义前头解释器会甩给你一个NameError: name greet is not defined。初学者常犯的错误是把函数定义放在文件末尾然后顶部直接调用。如果是因为主流程的逻辑更清晰而想把函数堆到底部可以这样做def main(): greet(张三) def greet(name): print(f你好{name}) if __name__ __main__: main()main()在文件末尾才被真正调用而调用它的时候前面的greet已经定义完成所以不会报错。这也是Python项目最常见的组织方式函数定义在前主入口在后。注意if __name__ __main__:这个判断的用途是“只有直接运行当前文件时才执行里面的代码被import时不执行”现在不理解没关系先把结构记住。2.3 空函数怎么写调试阶段经常需要先搭框架函数体还没想好。Python不允许空的函数体但你也不想写一堆占位垃圾代码。解决办法是用pass占位def command_parse(raw_input): # TODO: 解析输入格式这一步还没实现 passpass是一条什么都不做的语句专门用来占位让语法合法。类似的还有...Ellipsis也能起到相同效果def command_parse(raw_input): ...不过从可读性角度pass加注释比...更直观。记住pass不是注释它是实际执行的空操作只是副作用为零。3. 参数详解函数和外部世界的桥梁参数是函数设计的核心。设计得好的参数列表就像一把设计精巧的钥匙能适应多种锁又不会让使用者一头雾水。Python提供了整整一套参数体系我按从简单到复杂的顺序逐个拆。3.1 位置参数最朴素的传参方式调用函数时实参按顺序依次对应到形参这就是位置参数def introduce(name, age, city): print(f我叫{name}今年{age}岁来自{city}) introduce(张三, 25, 杭州)传入的三个值跟name、age、city一一对应。这套机制很直觉但有个明显的缺点——可读性差且容易错位。如果实参顺序颠倒了比如introduce(杭州, 25, 张三)程序不会报错但输出完全错乱。三个参数还好八个参数出错概率飙升。解决之道是使用关键字参数introduce(name张三, city杭州, age25)调用时指定形参名字Python会按名字对号入座顺序随便排。更妙的是位置参数和关键字参数可以混用introduce(张三, city杭州, age25)这里有两条硬性规则值得死记位置参数必须出现在关键字参数之前introduce(city杭州, 张三)直接语法错误同一个参数不能用两种方式传两次我把这些规则整理成一张速查表后面实操时候可以直接回来看传参方式示例适用场景纯位置参数introduce(张三, 25, 杭州)参数少、顺序好记、调用频率高纯关键字参数introduce(name张三, age25, city杭州)参数多、意义不直观的名称混合传参introduce(张三, age25, city杭州)前面几个必须传的用位置其他用关键字3.2 默认参数省去重复输入的烦恼很多函数参数有“绝大多数情况下都取这个值”的特征。比如写日志函数正常都要带时间戳但偶尔不需要。用默认参数解决def log_msg(message, levelINFO, with_timeTrue): if with_time: print(f[{datetime.now()}] [{level}] {message}) else: print(f[{level}] {message}) log_msg(服务启动) # 用默认级别和时间戳 log_msg(磁盘空间不足, WARNING) # 改级别时间戳照旧 log_msg(敏感操作审计, ERROR, False) # 三个参数全部覆盖默认参数的语法规则很简单带默认值的参数必须出现在不带默认值的参数右边。为什么如果写成def log_msg(message默认消息, level)调用时log_msg(外部输入)到底该把它当成message还是levelPython为了避免歧义直接禁止这种写法。这里有一个最著名的坑新手踩了无数次默认参数如果是可变对象列表、字典、集合函数会保留上次调用的状态。def add_item(item, container[]): container.append(item) return container print(add_item(苹果)) # [苹果] print(add_item(香蕉)) # [苹果, 香蕉]第二次调用结果出乎意料因为container[]这个列表在函数定义时被创建了一次之后所有调用共享同一个列表。正确做法是——默认值设为None函数体内每次新创建def add_item(item, containerNone): if container is None: container [] container.append(item) return container这个技巧的经验之谈是默认参数尽量用不可变对象None、数字、字符串、元组。id同一个对象导致的状态泄露问题排查起来极其隐蔽。3.3 可变参数*args与**kwargs有时候你写一个函数到底几个参数在调用时才能确定。比如求平均值、拼接多个字符串、配置几十个不确定键值的选项。这时候就要用到可变参数。*args接收任意多个位置参数打包成一个元组def average(*numbers): if len(numbers) 0: return 0 return sum(numbers) / len(numbers) print(average(1, 2, 3)) # 2.0 print(average(10, 20)) # 15.0 print(average()) # 0参数名不一定是args重点是单星号。*nums、*numbers、*nums_list都行但业界约定俗成用*args。函数内部numbers是一个元组可以遍历、切片、求长度。**kwargs接收任意多个关键字参数打包成一个字典def register_user(name, **info): print(f用户{name}) for key, value in info.items(): print(f{key}: {value}) register_user(张三, age20, city杭州, phone13800138000)**info把调用时多余的关键字参数收集成字典。用途很广写配置类、ORM查询条件、装饰器、类继承中的super().__init__等。组合起来的基本范式是def func(必需参数, *args, 默认参数, **kwargs): pass顺序规矩是普通参数 →*args→ 默认参数 →**kwargs。这里面有个细节*args后的默认参数必须用关键字传参def func(a, *args, b10): print(a, args, b) func(1, 2, 3, b100) # 输出: 1 (2, 3) 1003.4 强制关键字参数*的独特用法有时候你希望某些参数“只能用关键字方式传”避免使用者搞错顺序。Python支持一种天才般的语法——在参数列表里放一个单独的*def register_user(name, *, age, city): print(f{name}, {age}岁, {city}) register_user(张三, age20, city杭州) # 正常 register_user(张三, 20, 杭州) # TypeError: 多了位置参数*之后的参数被视为强制关键字参数。这种设计这么写的原因很实际——age和city都是容易混淆的概念禁止位置传参可以杜绝错序。Python很多内置函数都这么干比如sorted(iterable, *, keyNone, reverseFalse)强行让你写key...而不是sorted(lst, None, True)从源头规避低级失误。3.5 参数解包把列表字典拆开传与*args收集相反的操作是解包。一个函数接收三个固定参数你偏偏有一个装好三个值的列表可以用星号拆开def spread(a, b, c): return a b c nums [10, 20, 30] print(spread(*nums)) # 60字典也能用双星号解包成关键字参数config {a: 1, b: 2, c: 3} print(spread(**config)) # 6这两个技巧在封装接口、从配置文件读取参数时非常实用。记住一个对应关系一个星号拆可迭代对象当位置参数两个星号拆字典当关键字参数。4. 返回值详解函数与调用方的协议4.1 return到底做了什么函数计算的最终结果必须通过return送出去不然调用方拿到的全是None。遇到returnPython会做两件事立刻结束函数执行后面的代码不会跑把return后面的表达式的值返回给调用方def check_score(score): if score 60: return 及格 print(这句话只有在不及格时才会输出) return 不及格return可以出现在函数体的任意位置用多重return做条件早退是常见写法——效率高还减少了嵌套层级。4.2 多值返回的本质是元组一个函数能不能返回多个值能但要理解本质def min_max(nums): return min(nums), max(nums) # 看起来返回了两个值 result min_max([3, 1, 4, 1, 5]) print(result) # (1, 5)一个元组你其实是返回了一个元组Python自动把逗号分隔的表达式打包成元组。既然调用方拿到元组就可以用解构赋值low, high min_max([3, 1, 4, 1, 5]) print(f最小值{low}最大值{high})这种“多值返回 解构赋值”的组合是Python里非常优雅的惯用写法大量内建函数和第三方库都这么设计。4.3 没有return的函数返回None所有函数都有返回值即使你没写return。这时候Python隐式返回None。比如def do_nothing(): pass print(do_nothing()) # NoneNone在Python里是一个特殊的单例对象表示“什么都没有”。在函数设计中返回None有几层含义函数只负责副作用打印、写文件、改列表不产出计算结果函数正常结束但没找到合适的结果用None表示“无匹配”但这里有个常见的逻辑陷阱——布尔判断与None的混淆。写代码时注意区分result find_user(user_id) if result is None: print(用户不存在)而不是if not result: print(用户不存在)因为如果find_user返回空列表[]、空字符串、数字0它们也是“假值”但含义跟“用户不存在”可能完全不同。正确姿势是用is None判断保持逻辑精确。4.4 返回值的类型一致性这是很多函数设计问题的根源——同一个函数尽量不要一会儿返回字符串一会儿返回整数一会儿返回None。如果你面试造轮子或者维护一个被五十个函数调用的公共接口类型不统一的返回值会让调用方苦不堪言。反面例子def get_score(student_id): if student_id 001: return 90 elif student_id 002: return 缺考 else: return None你拿到这个结果想算平均值90 缺考直接TypeError。好的设计是def get_score(student_id): 返回成绩如果学生不存在或缺考返回None if student_id not in students: return None score students[student_id] return score if isinstance(score, int) else None至少在接口层面约定好——同一函数返回值类型一致。Python的类型注解def get_score(student_id: str) - Optional[int]:是你以后控制这个的有力工具但现在先建立意识。5. 作用域详解变量活在哪里作用域是我见过最让初学者头晕的概念之一但它的底层逻辑其实特别简单——你定义一个变量它能被谁看见5.1 LEGB法则Python查找变量时遵循LEGB规则顺序是L - Local局部作用域当前函数内部定义的变量E - Enclosing外层嵌套作用域外层函数里定义的变量闭包场景G - Global全局作用域模块顶层定义的变量B - Built-in内建作用域Python预置的len、print、range等看个例子x 全局x # Global def outer(): x 外层x # Enclosing相对inner是外层 def inner(): x 内层x # Local print(x) # 输出内层x inner() outer()程序执行到print(x)时按LEGB顺序先找Local里的x找到了就用它不会往上找。这个查找机制解释了“为什么我在函数里定义的变量外面用不了”。5.2 局部变量和全局变量默认情况下在函数内部给一个变量赋值Python把它当成全新的局部变量即使用这个名字跟全局变量一样count 10 def add_one(): count count 1 # UnboundLocalError这个报错是新手最经典的谜题。表面看count是全局的加一不就行了但Python的规则是——只要函数体内出现赋值语句解释器就在编译阶段把这个名字标记为局部变量。于是执行到count 1时count还是局部变量但还没赋值就报“使用前未绑定”的错误。解决办法是用global关键字声明count 10 def add_one(): global count count count 1 add_one() print(count) # 11但我得说句实话global尽量少用。全局变量把函数从“独立积木”变成了“依赖外部环境的顺风车”导致调试困难、函数无法复用。业务上实在需要共享状态优先考虑传参和返回值count 0 def add_one(n): return n 1 count add_one(count)这样add_one完全不依赖外部环境测试起来倍儿轻松。5.3 闭包与nonlocal声明有嵌套函数时内层函数可以访问外层函数的变量这叫闭包。但如果你想在内层函数里修改外层函数的变量global做不到因为它作用于模块全局在外层函数里定义一个变量之后用global会直接跑去动模块级变量。这时候需要nonlocaldef counter(): total 0 # Enclosing作用域 def increment(): nonlocal total # 声明要修改外层变量 total 1 return total return increment c1 counter() print(c1()) # 1 print(c1()) # 2 print(c1()) # 3闭包能实现“记忆功能”——函数记着上次调用的状态还不污染全局变量。装饰器模式就是基于闭包实现的以后你会大量遇到。nonlocal的关键在于它只向上找最近的Enclosing作用域不会管Local和Global。实际经验里闭包很强大但别滥用。状态多到五六个的时候写个类往往比闭包好维护得多。函数闭包适合状态少、逻辑精简的场景。5.4 作用域对函数设计的影响作用域规则对日常编程的启示有三条第一函数内部不要随意修改全局变量。真要改用return把新值传出来由调用方决定。这种单向数据流让代码可预测性大大提升。第二不要在函数内部直接依赖全局变量来修改逻辑。全局变量的存在让函数变“脏”——你没传它它却影响结果。如果某个值函数要用就明确通过参数传宁可调用麻烦点。第三正式代码里少用单字母变量名。也许在一个几十行的函数里i、x、t完全看得懂但当函数超过100行、嵌套三层循环之后单字母变量必然会让人抓狂。用index、position、elapsed_time花不了几秒打字却能让后来维护代码的人少掉很多头发。我用一个表格帮大家记忆重点场景访问外层变量没问题修改外层变量关键字函数内访问全局✔✖默认global内层函数访问外层✔✖默认nonlocal内层函数访问全局✔✖默认global“✔”只代表访问不代表修改。修改永远是那个需要显式声明的例外。6. 实操写一个成绩统计工具理论和语法讲了这么多我们直接做一个完整的小项目把这些知识串起来。需求写一个成绩统计模块支持录入学生成绩、计算平均分、判断及格、打印排名并统计不同分数段人数分布。6.1 明确功能拆分任何项目第一步不是写代码而是拆功能。这个成绩统计工具按函数划分record_score(name, score)录入一条成绩用全局字典存储average_score()计算平均分pass_rate()计算及格率rank_students()按分数从高到低排名score_distribution()统计优≥90、良80-89、中70-79、及格60-69、不及格60各组人数每个函数各司其职互不越界这就是函数式拆分的核心思想。6.2 代码实现成绩统计工具综合运用定义、参数、返回值与作用域 # 全局变量保存所有学生成绩 student_scores {} def record_score(name, score): 录入学生成绩。 参数: name: 学生姓名字符串 score: 学生分数数值0-100 返回: bool: 是否录入成功 if not (0 score 100): print(f分数不合法{score}应在0-100之间) return False student_scores[name] score print(f已录入{name} - {score}分) return True def average_score(scoresNone): 计算平均分。 参数: scores: 可选的成绩字典不传则使用全局数据 返回: float: 平均分没有成绩时返回0 if scores is None: scores student_scores if len(scores) 0: return 0.0 return sum(scores.values()) / len(scores) def pass_rate(scoresNone): 计算及格率≥60分。 if scores is None: scores student_scores if len(scores) 0: return 0.0 passed sum(1 for s in scores.values() if s 60) return passed / len(scores) * 100 def rank_students(scoresNone): 按分数降序排列学生。 返回: list[tuple]: [(分数, 姓名), ...] 从高到低 if scores is None: scores student_scores return sorted( [(score, name) for name, score in scores.items()], reverseTrue ) def score_distribution(scoresNone): 统计分数段人数分布。 返回: dict: {优: n, 良: n, 中: n, 及格: n, 不及格: n} if scores is None: scores student_scores levels {优: 0, 良: 0, 中: 0, 及格: 0, 不及格: 0} for s in scores.values(): if s 90: levels[优] 1 elif s 80: levels[良] 1 elif s 70: levels[中] 1 elif s 60: levels[及格] 1 else: levels[不及格] 1 return levels if __name__ __main__: record_score(张三, 88) record_score(李四, 59) record_score(王五, 93) record_score(赵六, 75) record_score(陈七, 43) print(f平均分{average_score():.1f}) print(f及格率{pass_rate():.1f}%) print(排名高到低) for score, name in rank_students(): print(f {name}: {score}分) print(分数段分布) for level, count in score_distribution().items(): print(f {level}: {count}人)运行结果已录入张三 - 88分 已录入李四 - 59分 已录入王五 - 93分 已录入赵六 - 75分 已录入陈七 - 43分 平均分71.6 及格率60.0% 排名高到低 王五: 93分 张三: 88分 赵六: 75分 李四: 59分 陈七: 43分 分数段分布 优: 1人 良: 1人 中: 1人 及格: 0人 不及格: 2人6.3 代码里的设计心思这段代码虽然短但每一处都有设计的考量逐条解读默认参数scoresNone的使用。我原本想设计成“不传就操作全局数据传入就用局部数据”这样既方便直接用全局数据跑也能随时传任意字典做单元测试。在def阶段我们说过默认参数别用可变对象——这里None是绝佳的占位符完美避开了共享状态坑。有return的录入函数。record_score返回了bool这样调用方可以判断录入是否成功决定下一步逻辑。如果它就是默默打印外部完全无法控制流程。真正的函数设计返回值是最重要的“沟通协议”。统计函数里的生成器表达式。sum(1 for s in scores.values() if s 60)这句本质上是一个极简的循环语义是“对满足条件的每一项贡献计数”。它等价于count 0 for s in scores.values(): if s 60: count 1在掌握了基础写法之后熟练使用这种表达式能大幅减少样板代码。if __name__ __main__:把主流程包起来了。以后你把这个模块import进其他项目不会触发测试代码的执行只有直接运行它才走主流程。这是Python模块化开发的惯例从第一课就养成这个习惯。7. 常见问题与排查技巧操作中踩坑是难免的把这些高频问题提前过一遍能省下大量抓耳挠腮的时间。7.1 常用参数相关报错速查表报错信息原因解决办法TypeError: missing 1 required positional argument: name少了必要位置参数调用时检查是否漏传参数或给参数设默认值TypeError: got multiple values for argument name同一个参数被位置传一次、关键字又传一次要么只用位置要么只用关键字TypeError: unexpected keyword argument xxx函数定义的参数列表里没有xxx检查参数名是否拼写正确或调整函数定义加**kwargsSyntaxError: non-default argument follows default argument默认参数前面还有非默认参数把无默认值的参数移到有默认值的参数前UnboundLocalError: local variable x referenced before assignment函数内对全局变量赋值Python识别成局部变量在函数开头加global x或用返回值绕过7.2 参数默认值共享的惨案刚才讲过def add_item(item, container[])的坑。这里再多说一句——其实很多教科书都会强调“不要用可变对象当默认参数”但大家依然会踩原因是它不会立即报错而是隔几次调用后突然蹦出诡异结果。我的建议是凡是默认参数需要用到列表、字典这类容器一律用None手动创建。这不是风格之争是bug的物理免疫。7.3 return位置写错初学者常把return写在for循环里面导致循环只跑第一次就提前退出def first_over_sixty(scores): for name, score in scores.items(): if score 60: return name # 只找第一个就返回了如果这不是你要的行为应该在外面收集结果def all_over_sixty(scores): result [] for name, score in scores.items(): if score 60: result.append(name) return result判别方法非常直接——return一执行函数就结束所以想累积多个结果把这个逻辑想清楚了再写。7.4 忘记调用括号这算是最隐蔽的错误之一def greet(): return 你好 print(greet) # function greet at 0x...你没有写括号Python不会报错它打印的是函数对象本身而不是调用结果。排查这类问题先在脑子里过一遍“这行代码执行的是函数体还是函数对象本身”这个问题。函数名加上括号才表示“执行它”没有括号就只是“引用它”。7.5 可变参数的顺序陷阱写带多个不同类型的参数时顺序写错是另一个高频报错。比如def func(a, *args, b10, **kwargs): pass注意b10必须放在*args后面否则Python无法区分这个默认参数究竟属于哪一类。想验证的同学可以试试这个顺序def func(a, b10, *args): # 这样其实合法但b不再是“强制关键字”了 print(a, b, args)func(1, 2, 3, 4)会得到1 2 (3, 4)b被位置参数2填掉3, 4进了args。这与你预期的“b是默认10args收集所有剩余参数”完全不同。所以如果想让默认参数不被位置参数抢占就把它放在星号后面。8. 写在最后函数是思维的单元这堂课的内容如果浓缩成一句话函数就是给一段逻辑起个名字并规定好输入和输出的边界。参数是你接收外部输入的方式返回值是你跟调用方的沟通协议作用域是你变量的领土边界。三个概念一盘棋缺一个函数都没法优雅地设计。我之前带过不少零基础学员观察到一个共性现象一开始写作业都把全部代码堆在平级的主流程里一改需求就全局变量乱飞。后来慢慢学会拆函数初学代码反而变得支离破碎——一个个小函数彼此割裂不知道谁调谁。这是正常的过程先学会拆再学会组装需要经历一个“从一团乱麻到流程清晰”的演化。随着练习增多你会慢慢掌握“多大的函数算合适”“哪些逻辑值得独立成函数”的手感那时候看代码就仿佛看到了脉络而不是平面的符号堆。实际操作中我最后再分享一个小技巧写函数时先写文档字符串再写函数体。哪怕心里只有一个模糊的“这个函数需要根据日期算出季度”先把根据日期返回季度这行字敲上去你会发现后面的实现思路会被这行文字牵引出来定义边界也顺理成章。这个习惯对我个人帮助极大从写小脚本到持续维护万行级别的项目都适用。下一步你可以试试自己写一个猜数字游戏把输入处理、逻辑判断、结果输出分别拆成独立函数——拆得越干净你就越能体会到函数式思维带来的清爽感。