Python函数完全指南:从基础语法到嵌套、闭包与项目实战

发布时间:2026/10/7 4:15:52
Python函数完全指南:从基础语法到嵌套、闭包与项目实战 我一直觉得学习 Python 函数这件事卡住大部分人的根本不是语法而是思维转变。语法就那几句——def、参数、return、调用死记硬背一天就能背完。但很多人写完能用的代码后一遇到要改需求或者代码变长就彻底崩盘到处复制粘贴、改一个地方漏三处。这篇博文就是要带你完成这个转变。我会把函数定义与调用的每个环节掰开揉碎讲清楚每个选择背后的为什么从基础语法一路推到嵌套函数、闭包、跨文件调用最后用一个完整实战项目把知识点全部串起来。不管你是刚装好 Python 还没写过几行代码的纯新手还是写过一些脚本但对函数总是感觉哪里不对的进阶学习者这篇文章都值得你跟着敲一遍代码。很多人问我Python 入门到底先学什么。我的答案一直没变过不是列表推导式、不是装饰器甚至不是类。就是函数。函数是所有结构化编程的基石也是你从写代码走向设计代码的第一道分水岭。你去看任何开源项目、任何框架源码抽丝剥茧到最底层全部是由一个个函数拼装出来的。函数这关过不了后面学类、学模块、学各种高级特性全都是在沙滩上盖楼。1. 别急着写代码先想清楚函数到底解决什么问题学习函数最大的障碍其实是不知道它好在哪里。大多数教材一上来就甩语法但你要先搞清楚我们到底在解决什么痛点。1.1 没有函数的代码长什么样我见过太多这样的代码。比如你写了一个学生成绩管理的小工具里面有三处地方都需要把分数转成等级的逻辑# 场景A单科成绩评级 score 87 if score 90: grade 优秀 elif score 80: grade 良好 elif score 60: grade 及格 else: grade 不及格 print(语文, grade) # 场景B数学成绩评级 score 54 if score 90: grade 优秀 elif score 80: grade 良好 elif score 60: grade 及格 else: grade 不及格 print(数学, grade) # 场景C平均分评级 avg 72.5 if avg 90: grade 优秀 elif avg 80: grade 良好 elif avg 60: grade 及格 else: grade 不及格 print(平均, grade)这段代码能跑但问题极其致命。假如学校改革了评级标准从90 分优秀改成85 分优秀你怎么办你得在代码里找三处 if 判断逐一修改。更恐怖的是这只是个小工具如果这种判断散落在几百个地方呢改注释、改漏一处、改错一处那是家常便饭。这就是没有函数的世界——代码靠复制粘贴堆叠业务规则散落各处维护成本随着代码量呈指数级上升。1.2 函数的三大价值复用、抽象、隔离函数这套机制恰恰同时解决了三个问题这也是它成为基石的根本原因。第一个价值是代码复用。把分数转等级的逻辑写进一个函数所有需要评级的地方都调用它。规则要改只改函数内部一处全程序生效。一次写成、多处调用——这就是可重用代码最朴素的定义。你写的函数就是你自己造的乐高积木今天拼个小车明天拼个大楼积木本身不用重新造。第二个价值是抽象。函数把复杂的实现细节藏起来只暴露一个简洁的接口。调用者只需要知道把分数传进去就能拿到等级根本不需要关心内部是 if 判断、字典映射、还是查数据库。这就像你开汽车只需要踩油门、打方向盘不需要每次开车都去研究喷油嘴的工作原理。当你把做什么和怎么做分开你的大脑才能在一个更高层次上审视整个程序。第三个价值是隔离。函数内部的变量默认和外部相互独立你在函数里创建临时变量不会污染全局环境。这听起来很平常但实际写代码时极其重要。没有函数的时候脚本跑着跑着变量就乱了——前面一个循环把i改了后面另一个循环直接跑飞。函数天然地把代码切成一个个独立的盒子每个盒子有自己的变量空间互不干扰。程序越写越长这种隔离能力就越珍贵。很多人以为函数就是把代码封装起来少写几行其实远远不止。它在逼你用模块化的思维拆解问题——把一个大任务拆成若干小任务每个小任务用一个函数实现再按逻辑顺序组合调用。这种拆解-实现-组合的思维方式才是函数真正想教会你的东西。2. 函数定义与调用的基础九连环思维问题想通了语法就水到渠成。这一节我把最核心的语法拆成几个连环扣每一步都讲透你跟着走一遍基本就不会再忘了。2.1 def、函数名、函数体、调用——缺一不可Python 定义函数的语法非常简单核心就一句话def 函数名(参数): 函数体 return 返回值用刚才的评级逻辑来示范def score_to_grade(score): if score 90: grade 优秀 elif score 80: grade 良好 elif score 60: grade 及格 else: grade 不及格 return grade # 调用 result score_to_grade(87) print(result) # 输出: 良好这里有几个极其容易被新手忽略的细节。def和函数名以及后面的圆括号、冒号构成了函数定义的头。圆括号里可以写参数也可以什么都不写但圆括号本身不能省略。无论有没有参数定义和调用时都要带着括号。我见过很多刚入门的朋友写print时不加括号直接写print hello——这在 Python 2 里是合法的但在 Python 3 里立刻报错。Python 3 的设计者就是故意逼你养成函数调用必须有括号的习惯。函数体必须缩进。Python 用缩进表示代码块的归属缩进不一致会直接报IndentationError。这一点和 Java、C 语言用花括号完全不同很多从其他语言转过来的新手不习惯但其实缩进反而是强制性的格式化让你的代码结构一眼就能看出来。调用函数的本质是执行函数体里的代码。程序执行到score_to_grade(87)这一行时会跳进函数体把 87 赋值给参数score然后从头到尾执行一遍函数体里的代码把返回值交给调用者再跳回原来的位置继续往下走。这个过程就是函数调用的本质——它不是在原地复制代码而是临时跳转执行。2.2 参数位置参数、关键字参数、默认参数参数是函数和外部世界交换信息的通道。Python 的参数系统非常灵活但核心就三种形态。位置参数是最直观的调用时传入的每个值按照顺序依次赋值给函数定义里的每个参数。def introduce(name, age, city): print(f{name}今年{age}岁来自{city}) introduce(张三, 25, 上海) # 输出: 张三今年25岁来自上海调用时传入的顺序必须和定义时一致。如果你写introduce(25, 张三, 上海)那么name就会变成 25程序不会报错但逻辑就全乱了。位置参数适合参数较少、顺序直观的场景。关键字参数是调用时用参数名值的方式传参你可以打乱顺序。introduce(city北京, age30, name李四) # 输出: 李四今年30岁来自北京这个特性在参数很多的时候特别有用——调用代码的可读性大幅提升你一眼就能看出每个值传给了哪个参数。混合使用时有个硬性规则位置参数必须先写关键字参数后写。introduce(王五, 28, city广州)合法introduce(name王五, 28, 广州)直接报语法错误。这个规则在 Python 里是强制性的背后逻辑是防止歧义——如果关键字参数在前后面的位置参数就没法判断该对应哪个位置。默认参数是在函数定义时直接给参数赋一个默认值def introduce(name, age, city未知): print(f{name}今年{age}岁来自{city}) introduce(张三, 25) # 用默认值: 来自未知 introduce(张三, 25, 杭州) # 覆盖默认值默认参数的意义在于让调用者可以省略某些不重要的参数同时保留定制的可能性。但这里有一个大坑很多老手都踩过我先按下不表放在第 8 节的常见问题里专门讲。你只需记住一条原则默认参数值不要用可变对象比如列表、字典要用就用None配合内部判断。2.3 返回值return 的三种形态return决定了函数交出什么。它有几种形态我帮你梳理清楚。第一种返回一个值。这是我们最常见的写法。return score把结果交给调用者调用处可以接住def add(a, b): return a b total add(3, 5) print(total) # 8第二种返回多个值。Python 的return可以同时返回多个值这非常实用——不需要像 C 语言那样传指针或者定义结构体。def get_min_max(numbers): return min(numbers), max(numbers) low, high get_min_max([3, 1, 4, 1, 5]) print(low, high) # 1 5这里的底层原理其实还是返回一个元组(1, 5)但 Python 的解包语法让调用者可以直接用两个变量来接。返回多个值时可以用return a, b, c调用处用对应数量的变量接住或者用一个变量接住整个元组再按索引取值。这个特性在写数据处理代码时极其常用。第三种也是新手最容易迷惑的没有return。如果函数体里没有写return函数执行完最后一行后会自动返回None给调用者。这个None是 Python 里的特殊值表示什么都没有。def say_hello(name): print(f你好{name}) result say_hello(小明) print(result) # None这里函数的真正作用不是返回一个值而是执行一个副作用打印到屏幕。你要想清楚自己写的函数到底是有返回值、作为计算工具还是没有返回值、作为动作触发器。很多人 debug 了半天发现函数输出结果是None其实就是因为忘了写return。在函数里print只是把内容显示到屏幕上并没有把结果交给调用者。还有一个细节return写在函数中间会立刻结束整个函数。def check_age(age): if age 18: return 未成年 return 成年虽然这段代码不写else也完全等价但早期的return相当于在函数里设了一个提前出口一旦条件满足后面的代码直接不再执行。这种写法叫早返回early return能让逻辑更清晰、嵌套层级更浅。写复杂逻辑时多用提前返回能显著降低代码复杂度。3. 参数传递与作用域踩坑重灾区如果你已经能把上面的基础语法用起来恭喜你已经可以写不少程序了。但函数真正的成人礼是搞懂参数传递和作用域。这两个知识点平时不显山不露水一出问题就让人抓狂。3.1 变量作用域局部变量、全局变量、global变量在函数内外是两个不同的世界。x 10 # 全局变量 def my_func(): y 20 # 局部变量 print(x) # 可以读取全局变量 x输出 10 my_func() # print(y) # 如果这行取消注释会报 NameError规则一函数内部可以读取外部的全局变量。规则二函数内部创建的变量比如y是局部变量函数执行结束后就销毁了外部访问不到。规则三函数内不能直接修改全局变量——或者说如果你直接给一个名字赋值Python 会认为你在创建一个局部变量。很多新手写这样的代码count 0 def increase(): count 1 # 报错: UnboundLocalError increase()为什么读count没问题但count 1就报错因为 Python 有一条编译期规则只要函数体里对某个名字有赋值操作这个名字在整个函数体里都被视为局部变量。count 1包含了对count的赋值所以 Python 认定count是局部变量。但赋值之前你又读取了count的值局部变量在赋值前没有值于是抛异常。如果想在函数内修改全局变量就用global声明count 0 def increase(): global count count 1 increase() print(count) # 13.2 可变参数传参的原地修改陷阱接下来是比global更隐蔽的一个坑。Python 参数传递的机制严格来说是按对象引用传递。这句话很绕我用实际代码给你演示。def add_item(item, lst): lst.append(item) print(函数内部:, lst) my_list [1, 2, 3] add_item(99, my_list) print(函数外部:, my_list) # 输出: # 函数内部: [1, 2, 3, 99] # 函数外部: [1, 2, 3, 99]看到了吗函数内部对列表的修改外部的my_list也变了。因为调用时传进函数的是列表对象的引用函数内的lst和函数外的my_list指向同一个列表对象。你在函数里append修改的是那个对象本身外部当然会感知到。这个特性本身不算 bug关键看你怎么用它。有时它是特性——你希望函数直接修改传入的数据结构。但更多时候它是隐患。比如你写一个处理用户列表的函数本意是返回一个新列表结果不小心把原始列表改了导致后续代码全出问题而且很难排查因为变量名在函数内外一模一样你根本看不出哪一步被改了。如果想避免这种原地修改最简单的办法是在函数内部先复制一份def add_item(item, lst): new_lst lst.copy() # 传入的是副本 new_lst.append(item) return new_lst或者对于字典用dict()、{**d}等方式创建副本。经验法则是如果函数不打算改变外部对象就先用.copy()或者创建新对象再操作如果函数就是干原地修改这件事的比如list.sort()那要明确命名和注释让调用者知道它会修改原对象。我在后面实战部分会再展示这个原则。3.3 尽量别用 global改成传入-返回模式虽然global是合法语法但我强烈建议你在实际项目中少用甚至不用它。为什么因为global破坏了函数最重要的隔离性。想象一下你的程序是个大系统有 10 个函数都依赖同一个全局变量。读写顺序稍微乱一点这个全局变量就被改得面目全非你根本不知道是哪个函数动了它。调试这种问题几乎只能靠人肉推理——在每个函数入口打印变量值慢慢排查。全局变量就像是公共食堂里的锅每个人都来炒两铲子最后饭菜坏了根本找不到责任人。更好的模式是传入-返回# 不推荐的写法 count 0 def increase(): global count count 1 # 推荐的写法 def increase(count): return count 1 count 0 count increase(count)第二种写法里函数是一个纯粹的输入-输出机器给它什么参数它返回什么结果不碰任何外部状态。这样的函数叫纯函数它最容易被测试、被复用、被并行执行也最不容易出 bug。你去看len()、sum()、min()这一票内置函数全部是纯函数清爽得不得了。4. 嵌套函数、闭包与 lambda把函数当值用走到这一步基础已经稳了。但 Python 的函数还可以更进一步——在函数内部再定义函数、把函数作为返回值、用 lambda 写出匿名函数。这一节的内容在热词里反复出现python函数嵌套定义和嵌套调用是进阶必须掌握的核心。4.1 函数内部再定义函数Python 允许在一个函数里定义另一个函数这种嵌套函数在内部被定义、被调用def outer(text): def inner(): print(f内层函数说: {text}) inner() outer(你好)嵌套函数的第一个价值是组织代码。当你有个复杂函数的实现需要拆分成几个小步骤但这些小步骤只服务于这个函数、不会被外部调用时就可以把它们定义为内部函数。这样外部模块不会多出一堆辅助函数的命名全局命名空间干干净净。第二个价值是访问外部函数的局部变量。内层函数可以引用外层函数的变量比如上面的text即使外层函数已经执行完毕内层函数依然记得这些变量。这是闭包的基础下面展开讲。4.2 闭包一个简单的计数器例子闭包closure是嵌套函数最精妙的用法内部函数不仅定义在外部函数里还被外部函数当作返回值抛了出来并且引用着外部函数的局部变量。用最经典的计数器例子来演示def make_counter(): count 0 def counter(): nonlocal count count 1 return count return counter my_counter make_counter() print(my_counter()) # 1 print(my_counter()) # 2 print(my_counter()) # 3这里的关键在于nonlocal关键字。没有它内层函数对count做 1也会遇到和前面global一样的坑——读得到但写不了。nonlocal告诉 Python这里的count不是我这个函数的局部变量请到外层函数的作用域去找并且允许我修改它。闭包初看有些抽象但你一定用过它的亲戚——装饰器。等你学到装饰器时回头再看这段会豁然开朗装饰器本质上就是一个闭包外层函数接收一个函数作为参数内层函数包装这个函数并返回。闭包还常用于配置包裹器——把一段配置和操纵它的函数捆绑在一起每次调用都用这份配置。我在实际项目里最常用的闭包场景是生成带前缀的日志函数def make_logger(prefix): def log(msg): print(f[{prefix}] {msg}) return log info_logger make_logger(INFO) error_logger make_logger(ERROR) info_logger(系统启动) # [INFO] 系统启动 error_logger(文件丢失) # [ERROR] 文件丢失两个日志函数共享同一套log逻辑但各自绑定不同的前缀。不需要重复写两遍打印代码这就是可重用代码的另一种形态——函数的函数。4.3 lambda一行写完的匿名函数lambda是 Python 里用来写一次性小函数的语法糖。它不用def、不用起名字直接表达式搞定double lambda x: x * 2 print(double(5)) # 10本质上lambda x: x * 2和def double(x): return x * 2没有区别只是更精简。lambda 最常见的用途是作为参数传给其他函数。比如给sorted()指定排序规则students [(小明, 85), (小红, 92), (小刚, 68)] sorted_by_score sorted(students, keylambda stu: stu[1]) print(sorted_by_score) # [(小刚, 68), (小明, 85), (小红, 92)]key参数接收一个函数这个函数从每个元素里取出排序依据。用def定义也可以但要先起名字再传进去不如 lambda 来得直接。类似场景还有map()、filter()以及后面学 pandas 时的apply()。不过我要提醒一句lambda 不是用来写复杂逻辑的。表达式超过一行、或者逻辑本来就复杂老老实实用def。很多人把 lambda 写出天书一行里嵌套两三个 lambda那代码你过三周再看自己都认不出来。Lambda 是做减法的工具不是炫技的舞台。5. 跨文件调用把你的函数变成小工具库热词里频繁出现跨文件调用这是个极其现实的场景。你不可能把所有函数都写在一个脚本里——一旦超过几百行维护就开始痛苦了。正确的做法是把一组相关的函数放进一个独立文件这个文件在 Python 里叫模块然后其他脚本导入使用。5.1 把函数放进独立模块并导入假设你和几个同事在开发一个项目你写了一个专门处理日期字符串的工具函数不想每次都复制粘贴。你创建了一个文件date_utils.py# date_utils.py def is_weekend(date_str): 简单的周末判断传入 20250330 格式的日期字符串 from datetime import datetime dt datetime.strptime(date_str, %Y%m%d) return dt.weekday() 5 def format_cn(date_str): 把 20250330 转成 2025年3月30日 return f{date_str[:4]}年{int(date_str[4:6])}月{int(date_str[6:8])}日在另一个脚本main.py中调用# main.py import date_utils print(date_utils.is_weekend(20250330)) print(date_utils.format_cn(20250330))用import 模块名导入后通过模块名.函数名()调用。如果觉得模块名太长可以用as起别名import date_utils as du之后写du.is_weekend(...)。也可以用from ... import ...直接把函数名导入当前命名空间from date_utils import is_weekend, format_cn print(is_weekend(20250330))两种方式各有取舍。import 模块名更明确——你知道每个函数来自哪个模块不会出现命名冲突from ... import *直接把模块里所有公开名字倒进来最省事但也最容易把命名空间搞乱。项目里我推荐多用import 模块名或精确的from 模块名 import 具体函数。5.2 ifname main 到底是什么意思几乎所有 Python 脚本里都能见到这句奇怪的代码if __name__ __main__: # 直接运行本文件时执行这里的代码 run_app()新手经常把它当作固定模板死记硬背。其实它的原理非常简单每个 Python 模块运行时都有一个内置变量__name__。当模块被直接运行时__name__的值是字符串__main__当模块被其他文件 import时__name__的值是模块自己的名字比如date_utils。所以这个判断的意思是只有当我这个文件被当作主程序直接运行时才执行下面的代码如果我只是被别人导入的模块就不执行。这非常重要。假设你在date_utils.py底部随手写了几句测试代码# date_utils.py def is_weekend(date_str): ... print(我在测试 is_weekend...) print(is_weekend(20250330))当main.py导入date_utils时这几行打印代码会立即执行把调试信息打到你的业务程序里。而如果你把测试代码包在if __name__ __main__:里只有直接运行date_utils.py时才会输出被 import 时完全静默。这个习惯一定要养成否则后续写包、写工具库时会吃大亏。5.3 用包组织函数让代码真正可扩展模块之上还有一层组织方式——包。包其实就是一个目录里面放着一堆模块文件外加一个__init__.py文件在 Python 3.3 之后甚至可以不强制有但为了兼容和显式声明建议保留。project/ ├── main.py └── utils/ ├── __init__.py ├── date_utils.py └── math_utils.py调用方式变成from utils.date_utils import format_cn from utils import math_utils包的好处是可以按业务领域组织你的函数库。utils目录里放通用小工具report目录里放报表相关逻辑data目录里放数据清洗函数。当你构建一个超过 10 个文件的项目时这种分层组织能力直接决定你三个月后还能不能看懂自己的代码。先把函数写清楚再把函数拆到模块最后用包组织模块——这是一条从写脚本到做项目的必经之路。6. 实战用函数拆解一个记账助手前面讲了这么多如果你还没动手写过现在是最好的时机。我们来做一个完整的实战项目一个命令行记账助手。这个项目非常适合练手因为它有清晰的输入输出、有数据存储、有统计逻辑足够展示函数的拆解与组合。6.1 需求与拆解思路需求很简单用户输入日期 分类 金额格式的账目比如2025-03-30 餐饮 25.5可以随时输入统计查看总支出可以输入查询 分类按分类查看记录输入退出结束程序拿到需求先别急着写代码用函数的思维拆解一下。这个程序有哪些独立的职责parse_input(text)解析原始输入把字符串变成(日期, 分类, 金额)这样的结构化数据add_record(records, record)往记录列表里追加一条total_spending(records)计算总支出filter_by_category(records, category)按分类过滤format_records(records)把记录列表格式化成好看的文本main()主循环负责读取用户输入、调用上述函数、输出结果每一个函数都职责单一互相独立。将来要改统计逻辑只动total_spending要改输入格式只动parse_input。6.2 完整代码与运行流程# expense_tracker.py def parse_input(text): parts text.split() if len(parts) ! 3: return None date, category, amount_str parts try: amount float(amount_str) except ValueError: return None return {date: date, category: category, amount: amount} def add_record(records, record): records.append(record) def total_spending(records): return sum(r[amount] for r in records) def filter_by_category(records, category): return [r for r in records if r[category] category] def format_records(records): lines [] for r in records: lines.append(f{r[date]} | {r[category]} | {r[amount]:.2f} 元) if not lines: return 没有记录 return \n.join(lines) def main(): records [] print(欢迎使用记账助手输入格式日期 分类 金额) print(例如2025-03-30 餐饮 25.5) print(可用命令统计 / 查询 分类 / 退出) while True: text input( ).strip() if text 退出: print(再见) break if text 统计: print(f总支出: {total_spending(records):.2f} 元) continue if text.startswith(查询 ): category text[3:].strip() result filter_by_category(records, category) print(format_records(result)) continue record parse_input(text) if record is None: print(输入格式有误请重新输入) continue add_record(records, record) print(已记录:, format_records([record])) if __name__ __main__: main()运行效果大致是 2025-03-30 餐饮 25.5 已记录: 2025-03-30 | 餐饮 | 25.50 元 2025-03-31 交通 6.0 已记录: 2025-03-31 | 交通 | 6.00 元 统计 总支出: 31.50 元 查询 餐饮 2025-03-30 | 餐饮 | 25.50 元 退出 再见这套代码的精髓在于main()这个主函数只负责流程控制所有实际计算都被拆到了独立的小函数里。你看main()的代码会非常舒服——每一行是在描述流程而不是在堆实现细节。记录列表records通过参数传给各个函数各函数返回结果主函数负责拼装。数据和行为分离逻辑一目了然。6.3 如何扩展函数化带来的优势现在你感受一下函数化的威力。假如新需求来了要支持修改和删除记录。你只需要新增两个函数def delete_record(records, index): if 0 index len(records): records.pop(index) return True return False def update_record(records, index, new_record): if 0 index len(records): records[index] new_record return True return False然后在main()的主循环里加两个分支即可。原有的total_spending、filter_by_category等函数一行都不用动。这就是可重用代码的意义需求永远在变但设计良好的函数可以在变化中保持不变成为稳定可靠的地基。如果你想让数据持久化只需要把records列表保存成 JSON 文件再写一个load_records()和一个save_records(records)。整个程序的骨架依然不动只是数据进出多了一层。这样的代码才是可以生长的代码——它不会因为需求膨胀而腐烂而是能不断接纳新功能越用越顺手。7. 递归、装饰器与趣味练习函数的进阶玩法到这里你已经具备扎实的函数功底了。再往深里走两步你会看到函数的另外两个经典场景递归和装饰器。热词里的李白打酒恰好是个经典的递归练习一并讲讲。7.1 递归函数调用自己递归就是函数直接或间接调用自己。它最典型的应用场景是解决结构上自相似的问题——比如遍历二叉树、计算阶乘、处理嵌套的字典。李白打酒是个经典题目李白提着酒壶出门遇店加一倍遇花喝一斗。他一共遇到店 5 次、花 10 次最后一次遇到的是花喝完壶中酒刚好空了。问壶里原来有多少酒。这个用递归的思路处理出口是遇到店的次数为 5、遇到花的次数为 10、最后一次是花、酒喝完了def libai_wine(store_left, flower_left, wine, last_is_flower): if store_left 0 and flower_left 0: return 1 if wine 0 and last_is_flower else 0 if wine 0 or flower_left 0 or store_left 0: return 0 count 0 # 假设这次遇店 if store_left 0: count libai_wine(store_left - 1, flower_left, wine * 2, False) # 假设这次遇花 if flower_left 0: count libai_wine(store_left, flower_left - 1, wine - 1, True) return count # 从某个酒量开始尝试找到符合条件的初始酒量 for w in range(1, 100): if libai_wine(5, 10, w, False) 0: print(初始酒量可能是:, w)递归的关键是三件事出口条件什么时候停止、递推公式当前步骤做什么、如何缩小子问题、边界防护防止参数越界导致死循环。写任何递归前先问自己这个问题的最简单形态是什么怎么从 n 的问题拆成 n-1 的问题递归不是炫技它只是用代码表达数学归纳法的思路。但也要注意Python 默认递归深度限制在 1000 层左右超过会报RecursionError。面试题偶尔考递归实际业务代码里能用循环就用循环除非树形结构天然适合递归。7.2 装饰器不改原函数给函数加功能装饰器是嵌套函数和闭包的集大成应用。它解决的问题是在不修改原函数代码的前提下给函数附加新的行为。比如你想给某些函数加一个运行时间日志import time def timed(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f{func.__name__} 耗时 {end - start:.4f} 秒) return result return wrapper timed def slow_function(): time.sleep(0.2) return done slow_function() # 输出: slow_function 耗时 0.2001 秒timed是语法糖等价于slow_function timed(slow_function)。timed返回一个闭包wrapper这个wrapper在调用原函数前后附加了计时逻辑。原函数slow_function本身没有任何改动——这就是装饰器最迷人的地方面向切面的增强与业务逻辑完全解耦。实际项目中装饰器常用来做登录鉴权、输入校验、日志记录、缓存。等你把装饰器用顺了回过头再看第 4 节的闭包和嵌套函数会有一种原来如此的顿悟感——一切高级特性都是基础语法垒叠出来的没有魔法。8. 常见问题与排查技巧实录最后这个部分是我这些年看别人踩坑和自己踩坑的实录。函数这一块报错翻来覆去就那几种但每种都有自己的姿势排查思路也各有讲究。8.1 报错速查表报错典型场景排查方向NameError: name xxx is not defined函数里用了没定义的变量检查变量名拼写是否忘记传参局部变量作用域UnboundLocalError: local variable xxx referenced before assignment函数内读写了同名变量函数内对变量有赋值操作Python 视为局部变量确认是否需要global或nonlocalTypeError: xxx() missing 1 required positional argument: yyy调用时参数数量不够数一下定义时的参数个数是否有默认参数可省略TypeError: xxx() takes 2 positional arguments but 3 were given传了太多参数检查是否把模块本身当作参数传给了函数IndentationError: unexpected indent缩进不一致检查混用空格和 Tab函数体未缩进函数返回了None忘了return检查函数结束处是否有returnprint不能替代return最阴险的是最后一类——不报错但结果不对。函数逻辑正确但result是None十有八九是写了print而不是return。另一个高频场景是函数内部修改了外部列表调用者不知道导致数据莫名其妙被改掉。这类静默错误比语法错误难查十倍网上的求助帖一搜一大片。8.2 默认参数与可变对象的经典陷阱我前面留了个扣子现在专门讲。看这段代码def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] ?! print(add_item(3)) # [1, 2, 3] ?!第二次调用输出的不是[2]而是[1, 2]。为什么因为默认参数是在函数定义时创建的而且只创建一次。每次调用如果没有传入lst使用的都是同一个列表对象上次调用 append 的内容当然还在里面。这几乎是 Python 最著名的坑之一。解决方案非常简单——默认参数用None函数内部再创建新对象def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst这个写法的原理是None是不可变对象每次调用都会重新走一遍lst []新列表就诞生了。记住了默认参数只接受不可变类型。字符串、数字、元组、None都行列表、字典、集合绝对不行。8.3 即时自测与调试小技巧最后一个板块分享几个我自己调试函数时常用的土办法不花哨但极其管用。第一个技巧是半路打印。在函数关键位置加print()看变量走到哪里断了。很多人觉得加打印丢人但调试的本质就是二分定位——在函数入口和出口各打一个如果入口有值、出口没值问题就在函数内部如果入口就没值问题在调用者。第二个技巧是在交互式环境里逐个函数测试。打开 Python 交互模式或者 Jupyterimport你的模块逐个调用每个函数喂几个代表性输入看输出是否符合预期。不要一上来就跑整个程序——那就像不看地图直接开车上高速迷路了根本不知道在哪下的口。第三个技巧是写小型断言。给函数写点自测代码养成习惯def add(a, b): return a b assert add(2, 3) 5 assert add(-1, 1) 0 print(测试通过)assert后面的表达式为假时程序会立刻抛出异常所以这一小段代码就能验证函数的核心行为。等你真正开始写项目这个习惯会救你无数次——每改一次代码就跑一遍断言比任何调试器都直观。最后一个建议也是我个人最想强调的给函数写文档字符串docstring。就是在函数开头用三引号写一句话说明这个函数是干嘛的、参数是什么、返回什么。Python 的help()函数会直接显示这段内容很多编辑器的悬停提示也会读取它。三个月后你回头看自己的代码靠的就是这一句句话来快速回忆。可重用代码不只是能用多次更是能被快速理解地复用。我在实际工作中体会最深的一点是函数命名比函数实现更重要。process_data()这种名字等于没写calculate_monthly_revenue(orders)一眼就明白职责。多花两分钟想一个准确的名字省下的是未来无数次这段代码到底干嘛的的翻查时间。函数是你和未来的自己之间沟通的桥梁落笔之前先想好你要说的话。