
我见过不少刚接触 Python 的朋友循环和条件判断还能跟着写一到函数定义与调用这里就卡住。症状通常很典型要么照抄教材代码却报 NameError要么定义了函数页面却一片空白要么弄不清参数到底该传什么。其实函数没那么玄它就是把一段会被反复用到的逻辑装进“盒子”定义是给盒子贴上标签、写明入口调用是从盒子里把结果取出来。这篇我就结合自己实际写代码和带新人的经验把 Python 函数的定义、调用、参数传递、嵌套递归、常见报错整个串一遍顺便用一道非常经典的“李白打酒”题做实战收尾。内容不会端着教材腔也没有故作高深的东西适合刚入门想真正搞懂函数的读者也适合写了一阵子但总在边界情况上翻车的人。1. 先搞懂函数到底是干嘛的再谈定义和调用1.1 函数不是什么高深概念它就是打包逻辑的盒子打个比方函数就像厨房里的一台咖啡机。你不需要每次冲咖啡都亲自烧水、磨豆、过滤你只需要按一个按钮把咖啡豆和水放进去机器自动完成里面的流程最后给你一杯咖啡。Python 函数也是这个思路把你反复要做的操作“打包”定义的时候写清楚输入什么、输出什么之后每次调用就直接把参数塞进去拿结果就行。很多初学者容易把“定义函数”和“调用函数”混为一谈。定义函数是描述过程调用函数才是真正执行过程。你光写了 def 不调用代码跑起来就跟没这回事一样。这个认知不扭转过来后面所有问题都会变得莫名其妙。那什么时候应该写函数我自己的经验是三个信号同一段逻辑在代码里出现了两次以上强烈建议抽成函数一段逻辑特别长哪怕只出现一次也建议拆成几个函数方便读懂和调试后续大概率要改动的逻辑比如接口地址、判断规则、过滤条件抽成函数后只需要改一个地方。这三点不是死规矩但按照这个原则去写代码的维护成本会明显降下来。你在小项目里可能感觉不到项目一变大到处复制粘贴代码改一个需求就得全局搜索替换那时候你就会明白函数的价值了。1.2 定义一个函数只需要四要素def、函数名、参数、返回值Python 定义函数的标准模样是这个def greet(name): message 你好 name return message拆开来看就是四个要素def关键字告诉 Python 我要开始定义一个函数了函数名greet相当于给这个咖啡机起的名字之后调用全靠它参数name函数入口传入什么值决定输出什么结果return message把计算结果交还给调用者。第四部分也就是返回值。可以用 return 返回一个值也可以写多个值也可以不写 return此时函数执行完之后会返回一个 None。很多新人以为不写 return 函数就“什么都没干”实际它确实可能干了不少事只是对外没有返回结果而已比如改了一个全局文件、打印了一行日志至于返回值是 None 这件事经常被忽略。我建议初学者脑子里建立一个公式函数 输入(参数) 处理(函数体) 输出(return)。不管是多复杂的函数本质上都是这三段。后面你看到装饰器、生成器、闭包这些进阶概念会发现它们也无非是在这三段上做文章。1.3 调用函数的时机定义先于调用不执行就没有结果Python 代码是从上到下逐行执行的。这意味着你在调用一个函数之前必须保证 Python 解释器已经见过这个函数的“定义”。如果你在文件顶部直接调用greet(张三)但greet的定义写在了文件底部运行时会直接给你抛NameError: name greet is not defined。这个坑很多人都踩过包括我自己刚开始写脚本时也遇到过。解决办法有两个方向把函数定义放在调用之前这是最直观的做法小脚本完全够用用一个main函数把所有调用包起来然后通过if __name__ __main__:在文件末尾或者文件中间触发调用。def greet(name): return 你好 name def main(): print(greet(张三)) if __name__ __main__: main()第二种写法在工程里更常见因为当你的函数被其他模块 import 时不会一导入就立刻执行一堆代码只有在直接运行当前文件时才触发 main。这个细节现在不深究但你要知道有这个机制写稍微大点的项目时非常关键。2. 定义函数参数和返回值的各种玩法2.1 参数类型位置参数、关键字参数、默认参数、*args 和 **kwargs新手对参数最大的困惑往往是“这个函数到底传几个参数”。最基础的参数叫位置参数调用时按顺序传。def add(a, b): return a b print(add(3, 5))这里 3 传给 a5 传给 b顺序不能乱。你把 3 和 5 对调结果可能完全不对。接着是关键字参数调用时直接用参数名指定值这时候顺序就无所谓了。print(add(b5, a3))关键字参数在处理多个参数的场景下尤其好用。比如一个函数有六个参数你要传第四个和第六个中间两个用默认值你直接用关键字传那两个就行不用管其他人。默认参数更是实用。定义时为参数赋予一个默认值调用时如果没传就用默认值def greet(name, greeting你好): return greeting name print(greet(李四)) # 输出你好李四 print(greet(王五, 早上好)) # 输出早上好王五默认参数有个经典大坑我单独提醒一句默认参数不要用可变对象比如列表、字典。你如果写了def func(items[])这个空列表只会在定义函数时创建一次后面所有调用共用同一个列表改了 A 次调用的结果会影响 B 次调用。正确写法是def func(itemsNone)在函数体内判断if items is None: items []。最后是两种收尾参数。*args收集多余的位置参数打包成一个元组**kwargs收集多余的关键字参数打包成一个字典。def log(level, *args, **kwargs): print(级别, level) print(位置参数, args) print(关键字参数, kwargs) log(INFO, 第一条, 第二条, user张三, count3)你可能觉得这种写法花里胡哨实际在框架代码里很常见。比如写一个日志装饰器根本不知道使用方会传什么参数用*args, **kwargs把参数原封不动接收再转发就很舒服。2.2 返回值return 的本质和返回多个值return 的作用是结束函数并把一个结果返回给调用处。有些新手误解 return 只是“打印一下”所以习惯在函数里写print而不写 return。如果你下次要把函数的输出继续参与运算就发现拿不到值。def square(x): return x * x result square(5) print(result 1) # 输出 26如果square里面写的是print(x * x)那么result就是 None完全没法继续加 1。Python 函数可以同时返回多个值实际上返回的是一个元组。def compute(a, b): return a b, a - b, a * b sum_result, diff_result, product_result compute(10, 3)这种写法在需要返回状态码、结果数据和错误提示时很常见比如def find_user(user_id): if user_id ! 1: return False, 用户不存在 return True, {id: 1, name: 张三}函数调用方就可以用两个变量接住结果一边判断是否成功一边处理数据逻辑清晰也不需要用字典包一层再一个个取 key。2.3 嵌套定义和嵌套调用函数内部再套函数Python 允许在一个函数内部定义另一个函数这就是嵌套定义。被套在里面的函数称为内层函数它只能在外层函数作用域范围内被访问外部拿不到。举个实际例子def outer(text): def inner(): return text.upper() return inner() print(outer(hello))这里inner在outer函数内部定义可以直接捕获外层函数的局部变量text这种“函数里定义函数并捕获外部变量”的行为是后面学闭包的基础。嵌套函数的好处是如果某个辅助逻辑只服务于当前函数完全没必要把它暴露到全局避免污染命名空间。比如你要清洗一段文本清洗规则很细但只有当前函数用得到那就直接内层定义。嵌套调用则更常见指的是函数 A 的内部调用了函数 B。函数之间可以层层套着调用这正是模块化思想的体现。def format_number(n): return f{n:,.2f} def report(price, quantity): total price * quantity return 总价 format_number(total) print(report(12.5, 3)) # 总价37.50report内部调用了format_number一个负责计算一个负责格式化各司其职。2.4 函数参数传递的底层理解很多教材会讲“值传递”和“引用传递”Python 里的说法其实是“对象引用传递”。简单理解就是参数传给函数时传递的是对象引用而不是对象本身的拷贝。对于不可变对象整数、字符串、元组函数内部修改不会影响外部变量对于可变对象列表、字典、集合函数内部原地修改会影响外部。def add_item(item_list, item): item_list.append(item) my_list [1, 2, 3] add_item(my_list, 4) print(my_list) # [1, 2, 3, 4]这里 my_list 是列表是可变对象函数内部 append 直接改了原列表。如果你不想要这种效果在调用前用my_list.copy()复制一份再传或者函数内部用新变量操作。这个知识点怎么强调都不为过。我在工作里见过因为这个问题导致线上数据被污染的事故一大串排查下来发现就是某个函数内部原地修改了外部传入的字典。写函数时心里要有个默认原则除非你明确想修改外部可变对象否则尽量别在函数体内原地修改传入的列表和字典。3. 调用函数作用域、生命周期、递归与回调3.1 局部变量和全局变量调用时最容易踩的坑函数内部定义的变量是局部变量它的生命周期和函数调用绑定。函数执行完局部变量就被释放。函数外部定义的变量是全局变量从定义处开始整个模块可见。新手最容易犯的错是在函数内部想修改一个全局变量直接写赋值语句结果报错或者改了没效果。count 0 def increase(): count count 1 # 这里会报 UnboundLocalError原因是在函数内部对count赋值Python 会把它当成一个新的局部变量而count 1在右侧先引用了局部变量此时局部变量还没绑定任何值所以报错。如果你确实要修改全局变量需要用global声明count 0 def increase(): global count count count 1不过我建议能用参数传递和返回值解决就别依赖 global。全局变量多了之后函数之间的隐式依赖会变得非常复杂调试时你根本不知道是哪个函数在哪一步给全局变量改成了什么值。我个人习惯是全局变量只放常量比如配置项、路径常量可变状态全部通过参数传入、结果返回。再看一个更隐蔽的例子不同函数间的变量传递def greet(name): return 你好 name def main(): name 张三 print(greet(name))name在main里是局部变量但它作为参数传给greetgreet内部通过name这个参数名引用的是同一个字符串对象。你只要弄清楚“函数之间的数据传递靠参数和返回值不靠共享变量”大部分作用域问题就迎刃而解了。3.2 函数是第一等对象可以被赋值、放进列表、当参数传递Python 中函数本身也是对象。这意味着你可以把函数赋值给另一个变量可以放进列表里可以作为参数传入另一个函数。def square(x): return x * x def cube(x): return x * x * x operations [square, cube] for op in operations: print(op(3)) # 依次输出 9 和 27把函数当参数传递就是回调函数的核心。def process(data, callback): result callback(data) return result def double(x): return x * 2 print(process(5, double)) # 10这种写法适合把“变化的部分”从“固定的流程”里抽离出来。比如数据处理流程是固定的读取、预处理、输出但“预处理”的具体规则不确定就可以设计成参数让调用方自己传函数进来。很多 Python 内置函数也接受函数作为参数比如map、filter、sorteditems [3, 1, 4, 1, 5, 9, 2] sorted_items sorted(items, keylambda x: -x) print(sorted_items) # 降序排列这里key参数就传入了一个匿名函数 lambda。lambda 本质上是没有名字的简写函数适合那些只使用一次、逻辑简单的场景。3.3 递归调用函数自己调用自己必须设置出口递归是函数调用中让很多新手头疼的知识点。递归的核心只有两件事函数调用自己 设置终止条件。典型例子是计算阶乘def factorial(n): if n 1: return 1 return n * factorial(n - 1) print(factorial(5)) # 120执行过程可以理解成层层递进factorial(5) 等待 factorial(4) 的结果factorial(4) 等待 factorial(3) 的结果一直递到 factorial(1)此时不再递归返回 1然后再一层层回推得到 120。没有终止条件的递归会无限调用下去最终触发RecursionError。Python 默认递归深度限制在 1000 层左右你可以用sys.setrecursionlimit调高但一般不建议因为递归太深容易爆栈而且性能也差。我自己的建议是能用循环解决就用循环如果问题本质上是“分形”结构比如遍历树形目录、解析嵌套括号、回溯求解用递归写起来会自然得多此时才选递归。不要为了炫技而递归。3.4 模块化调用函数组合的意义写函数不只是为了减少重复代码更是为了把大问题拆成小问题。我常对新手说如果你发现自己的函数里面全是 if 嵌套和长串逻辑先别急着优化里面的代码试试看能不能拆成两三个小函数。比如你想实现一个用户注册功能def validate_username(username): return len(username) 3 def validate_password(password): return len(password) 6 and any(c.isdigit() for c in password) def register(username, password): if not validate_username(username): return False, 用户名长度不足 if not validate_password(password): return False, 密码强度不够 # 假装修复后的后续逻辑 return True, 注册成功每个函数都短小、职责单一调用关系一目了然。这种组合方式是函数最重要的价值。你在面试里被问“怎么提升代码可读性”第一反应可能不是设计模式而是把长函数拆成多个有清晰职责的小函数多数面试官都会认可这个方向。4. 实战演练用函数解一道经典的“李白打酒”题4.1 题目拆解把问题转化成递归函数的输入输出“李白街上走提壶去买酒。遇店加一倍见花喝一斗。五遇店和花喝光壶中酒。试问壶中原有多少酒”这里面的事件是“店”和“花”店的规则酒量变 2 倍花的规则酒量减 1。总事件次数是 10 次其中店 5 次、花 5 次并且满足“五遇店和花”顺序是店和花穿插的任意排列最后酒量正好是 0。这个问题用来练函数特别合适因为它天然适合递归回溯。我们不需要直接推导原酒量而是枚举所有可能的事件顺序筛选出满足条件的那些再反推起始酒量。我们要用函数来抽象两件事模拟一次事件对酒量的影响递归枚举所有剩余事件排列。设计递归函数时把它想成一个“状态转移器”给定当前酒量、还剩几家店、还剩几朵花接下来怎么走。函数返回值我们设计成布尔值或者列表这里我先用布尔值加打印路径。4.2 代码实现递归函数 回溯下面是完整代码我会逐段解释。def solve(wine, shop_left, flower_left, path): # 终止条件店和花都用完了 if shop_left 0 and flower_left 0: if wine 0: print(path) return # 遇到店酒量翻倍 if shop_left 0: solve(wine * 2, shop_left - 1, flower_left, path 店) # 遇到花酒量减一 if flower_left 0: solve(wine - 1, shop_left, flower_left - 1, path 花)初始状态酒量是未知数 wine店剩 5 家花剩 5 朵路径为空字符串。调用时从 0 到某个上限遍历初始酒量。for w in range(1, 21): print(初始酒量, w) solve(w, 5, 5, )递归中的剪枝操作可以帮助提高效率。如果当前酒量已经为负数或者酒量大于剩余事件处理之后的上限可以提前 returndef solve(wine, shop_left, flower_left, path): if wine 0: return if shop_left 0 and flower_left 0: if wine 0: print(path) return if shop_left 0: solve(wine * 2, shop_left - 1, flower_left, path 店) if flower_left 0: solve(wine - 1, shop_left, flower_left - 1, path 花)运行效果是这样的程序会打印所有满足条件的事件顺序。你会发现初始酒量不是唯一解比如某些初始酒量下存在多种店花排列结果。我实际跑下来发现初始酒量为 0 或 1 左右在早期就会因为酒量变负被剪枝而符合条件的答案通常落在几个整数范围附近。这个问题如果手推非常容易漏解用递归把它翻译成“状态转移 终止判断”之后就成了一个标准的函数定义与调用练习。你定义了一个solve函数它接收四个参数自己在内部调用自己这就是递归调用的完整落地。你可以改用返回值收集所有合法路径而不是直接 print这样函数就成了一个纯计算函数def solve(wine, shop_left, flower_left, path, results): if wine 0: return if shop_left 0 and flower_left 0: if wine 0: results.append(path) return if shop_left 0: solve(wine * 2, shop_left - 1, flower_left, path 店, results) if flower_left 0: solve(wine - 1, shop_left, flower_left - 1, path 花, results) def find_solutions(max_wine): all_results [] for w in range(1, max_wine 1): results [] solve(w, 5, 5, , results) for p in results: all_results.append(f初始酒量 {w}: {p}) return all_results for item in find_solutions(10): print(item)这里又体会到了返回值的好处find_solutions返回一个列表你拿到后可以做筛选、排序、统计而不只是打印。4.3 从递归回溯延伸到函数组合的思路李白打酒这道题还能再拆分可以抽出一个drink事件处理函数把店和花的逻辑抽成两个小函数让代码更贴合业务语义。def meet_shop(wine): return wine * 2 def meet_flower(wine): return wine - 1 def solve(wine, shop_left, flower_left, path, results): if wine 0: return if shop_left 0 and flower_left 0: if wine 0: results.append(path) return if shop_left 0: solve(meet_shop(wine), shop_left - 1, flower_left, path 店, results) if flower_left 0: solve(meet_flower(wine), shop_left, flower_left - 1, path 花, results)这样改完业务逻辑与递归流程就分离了。以后如果规则变了比如“遇店酒量变三倍”只需要改meet_shop一个函数其他部分不用动。这种小重构就是函数组合的日常实践。5. 函数相关的常见报错与排查记录5.1 环境坑pip 无法识别、Python 没进 PATH如果你刚安装 Python 就遇到终端报错pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这大概率是 Python 安装时没有勾选“Add Python to PATH”导致系统找不到pip命令。这虽然不是函数定义的报错但会直接打断你“先跑起来再学函数”的节奏所以放在这里一起说。两个解决办法重新运行 Python 安装程序点击 Modify勾选 Add Python to PATH手动找到 Python 安装目录下的Scripts文件夹把它加到系统环境变量 PATH 里。另外一个同款问题报python 无法识别也是 PATH 没配好。配置完之后新开一个终端窗口再验证python --version通常就能正常用了。5.2 代码坑NameError、缩进错误、默认参数、作用域问题函数相关最常碰到的报错我整理成表格方便你对照报错信息典型原因排查思路NameError: name xxx is not defined函数名或变量名拼写错误、定义晚于调用找到定义位置检查拼写把定义移到调用前IndentationError: expected an indented blockdef 下面的函数体没缩进或缩进不一致用四个空格统一缩进不要混用 Tab 和空格UnboundLocalError: local variable x referenced before assignment函数内赋值了与全局变量同名的变量想改全局变量就用 global不想改就换参数传递TypeError: add() missing 1 required positional argument: b调用时少传参数检查函数定义所需参数数量和调用处实际传参TypeError: add() got multiple values for argument a位置参数和关键字参数重复传了同一个参数要么都用位置参数要么避免对同一个参数既传位置又传关键字RecursionError: maximum recursion depth exceeded递归没有正确返回终止条件检查递归函数里有没有终点分支默认参数可变对象更特殊不会直接报错但结果会错得莫名其妙。我自己就遇到过def add_task(task, task_list[]): task_list.append(task) return task_list print(add_task(任务A)) print(add_task(任务B))第二次调用输出的列表里会包含“任务A”这通常不是预期结果。上面提过了默认参数改成None在函数体内创建新列表。5.3 设计函数时的十四条经验清单最后整理一份我从实际工作中提炼的清单你可以直接当小抄函数名用动词开头比如get_user、validate_age从名字就能看出它想干嘛一个函数只干一件事超过两三件事就拆参数数量尽量控制在三个以内参数太多了可以考虑合并成一个配置对象不要随意修改传入的可变对象需要修改就返回一个新对象return 语句尽量只出现在函数的末尾提前返回要注释说明原因写注释解释“为什么”而不是“是什么”代码本身已经说明了做什么函数内部不要塞大量全局变量数据传递走参数和返回值递归时先想清楚终止条件再考虑递推逻辑打印调试没问题但调试完要记得把多余 print 清理掉一个模块里函数数量过多考虑按职责拆到多个模块文件函数定义和调用在代码风格上要保持一致参数对齐、空行规范别怕用嵌套函数但外部不可见的功能才放内部学会用 lambda 处理简单的回调逻辑复杂逻辑还是单独定义函数函数写完之后花一分钟想想别人拿到这个函数光看名字和参数能不能猜出它返回什么我在实际带新人的过程中发现函数掌握得好的标准从来不是能背出多少语法而是拿到一个新需求先能画出“这个程序需要哪几个函数、谁调用谁、输入输出是什么”的草图。画出来之后实现永远是最简单的部分。Python 函数定义与调用这门基本功练到“条件反射”的程度后面学面向对象、装饰器、生成器都会顺很多。你先从给手头脚本里的重复代码抽函数开始跑通了再回头读这篇文章感受会完全不同。