
函数这种东西早期写 Python 的时候真没太当回事。反正脚本往下写变量到处用逻辑复制粘贴改两行也能跑出结果。等代码量上来一个文件上千行每次改需求都要在一堆重复代码里精准定位、逐处修改那种痛苦会逼着你重新审视这个叫“函数”的东西到底在干嘛。后来才明白函数不是给机器看的是给人看的——它把一个个会重复执行的动作打包成可复用的积木块让代码从“章节式的叙述”变成“模块化的工具箱”。这篇文章我就打算从 Python 函数的基础说起围绕函数的参数、返回值、作用域、高阶用法这几个核心点结合真实场景聊聊怎么把“代码复用”这个听起来很虚的概念落到实际代码里。适合刚学 Python、感觉逻辑还没理顺的初学者也适合会写但总觉得代码很“脆”、想优化一下代码结构的同学参考。1. 函数把“重复动作”变成“可调用的积木块”1.1 为什么非要函数不可先抛开语法想一个生活场景。你每天早上出门前要做三件事摸钥匙、关煤气、锁门。如果每天都把这三步原封不动写一遍那早起十分钟都写不完。更好做法是给这套动作起个名字叫“出门检查”以后每天只需要说一句“该出门检查了”大脑就自动执行三步。函数干的就是这件事——把一组固定动作打包成一个命名块调用一次执行一次而不用每次都从头写一遍。放到代码里重复的场景特别常见。比如你在数据处理脚本里动不动就要“把 DataFrame 中某列的空值填成 0”如果每次都写一遍这个逻辑代码会非常臃肿。一旦需求变了比如改成填均值你得把所有出现过这个逻辑的地方全部找出来一个个改漏一处结果就错一处。这个问题本质上是“修改的地方不够收敛”而函数恰好能把逻辑收拢到一个位置让所有调用方自动获得更新后的行为。这其实就是“DRY 原则”——Dont Repeat Yourself不重复自己。函数是 Python 里最基础的代码复用手段也是模块化设计的起点。你不需要一上来就理解面向对象只要把函数用好了代码的可读性和可维护性就能提升一大截。1.2 函数定义与调用的最小模型Python 里定义函数用def关键字语法很简单def greet(name): print(你好 name )这里的def是关键字greet是函数名name是参数冒号后面缩进的代码块就是函数体调用时执行。调用方式是greet(张三)这时候函数体内的name会被替换成字符串张三输出“你好张三”。有几个细节是初学者特别容易踩坑的缩进必须保持一致。函数体里的代码块通常缩进 4 个空格混用 Tab 和空格会直接报IndentationError。函数定义之后只有调用了才会执行。也就是说def只是声明了一个“能力”真正触发是在你写上greet(张三)的那一刻。函数名要能表达用途。叫greet比叫a或者fun1清晰得多。命名混乱的代码即使逻辑正确别人读起来也会非常吃力三个月后的你自己也一样。关于环境准备再啰嗦一句在正式练习之前先把 Python 装好并激活命令行环境。如果在命令行里输入python提示找不到命令或者执行pip install xx提示“无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”多半是安装时没有勾选“Add Python to PATH”或者安装完没有重启终端。这时候去系统环境变量的 Path 里把 Python 安装目录和 Scripts 子目录加进去通常就能解决。我见过太多刚入门的朋友卡在这一步不是代码写不对是运行环境压根没通。1.3 函数是“动作”的封装不只是“计算”很多人理解函数是“输入→输出”的计算器这个理解不算错但不完整。函数可以没有参数、没有返回值它还能封装“动作”本身——比如上面打印问候语这个函数没有计算发生只是执行了特定行为。再比如一个发送邮件的函数输入是“收件人、主题、正文”做的事是“连接服务器、构造邮件、发送”返回的可能是发送结果或者什么都不返回。我当初学函数有个误区总觉得一个函数一定要有返回值没有return就是没写完整。后来才知道在 Python 里没有return的函数实际上会隐式返回None这种函数通常叫“过程”聚焦在执行步骤而不是产生结果。理解了“函数既可以算值也可以做动作”你对函数定位的认识会宽很多。现实中的函数往往混合两者先做一系列动作最后返回一个计算结果。所以设计函数的时候第一步要想清楚这个东西是“做事的”还是“算数的”还是两者都有想清楚了参数和返回值的安排就自然出来了。2. 参数机制函数从“固定流程”变成“通用工厂”的关键2.1 参数让函数有了“弹性”如果函数内部全是写死的逻辑那和复制粘贴代码没有本质区别。参数的作用是让函数在处理大体相似、细节不同的场景时不必重建一套流程而是往同一个流程里注入不同的“原料”。看一个具体例子。你要写一个计算打折后价格的函数def discount_price(price, rate0.8): return price * rate调用discount_price(100)结果是 80调用discount_price(100, 0.5)结果是 50。这里的rate0.8是默认参数。注意默认参数值的放置顺序没有默认值的price必须放在前面带默认值的rate放在后面否则 Python 会直接抛SyntaxError。关于参数类型再拓展两个概念位置参数按顺序传进去的参数实参与形参按位置一一对应。关键字参数调用时用参数名值的方式传入可以不关心顺序。所以你也可以这样调用discount_price(rate0.5, price100)。这种写法可读性更高尤其是参数很多的时候一眼就能看出某个值对应的是什么含义。2.2 可变参数不确定数量的参数就用它现实中函数要处理的参数个数往往不确定。比如sum_all(1, 2, 3)和sum_all(1, 2, 3, 4, 5)都要支持怎么办Python 里的*args就是干这个的def sum_all(*args): total 0 for n in args: total n return total这里的args其实是一个元组把调用时传入的所有位置参数打包进去。类似地**kwargs会把多余的关键字参数打包进一个字典def print_info(**kwargs): for key, value in kwargs.items(): print(f{key}: {value}) print_info(name张三, age18)这两者的命名并不是关键字完全可以叫*numbers、**options但*args和**kwargs是社区约定俗成的写法大多数开源代码都这么写你最好也延续这个习惯读别人的代码会顺畅很多。2.3 参数传递到底是“值传递”还是“引用传递”这是函数进阶必踩的坑也是面试常客。Python 里传参的准确说法是“传对象引用”实际上传的是一个指向对象的“引用”。如果参数是整数、字符串、元组这类不可变对象函数内部重新赋值不会影响外部变量def add_one(x): x 1 print(x) # 结果是 11 a 10 add_one(a) print(a) # 外部仍然是 10因为x 1实际上创建了新的整数对象外部a压根没动。但如果参数是列表、字典这类可变对象情况就不一样了def append_item(lst): lst.append(100) my_list [1, 2, 3] append_item(my_list) print(my_list) # 输出 [1, 2, 3, 100]函数内部直接修改了 list 本身外部变量自然跟着变了。这个特性有时是优势模拟“引用”效果更多时候是隐患——你不小心在函数里改了外部数据排查半天都找不到源头。所以我个人有个习惯如果函数不需要修改传入的可变对象就先用.copy()复制一份或者明确在函数命名上体现“会被修改”的意图比如add_item_to、update_config这种前缀。代码的意图坦白了坑就少了一半。2.4 可变默认参数设计函数时最隐蔽的深坑有经验的 Python 开发者都知道这个经典陷阱默认参数如果用的是可变对象函数被多次调用时默认值会在调用之间“残留”。看代码def add_student(student, students[]): students.append(student) return students print(add_student(张三)) # [张三] print(add_student(李四)) # [张三, 李四]第二次调用时students不再是空列表而是上一次调用留下的[张三]。原因是函数定义时默认参数[]只创建了一次所有调用共享的是同一个列表对象。这个问题的标准解法是默认参数设置为None函数体内部再创建新列表def add_student(student, studentsNone): if students is None: students [] students.append(student) return students每次调用都会新建一个独立的空列表逻辑才符合直觉。这是我写函数时反复提醒自己的一条铁律默认参数只用不可变值可变对象一律用None占位。3. 返回值函数与调用方之间的“交接协议”3.1 return 的用法与多值返回函数通过return把结果交回给调用方。需要注意的是return一旦执行函数立刻结束后面的代码不会运行。所以有时候把return放在条件判断里可以起到“提前退出”的作用避免嵌套层级过深def parse_config(data): if not data: return None # 继续处理 data... result data.strip() return resultPython 的return还能一次返回多个值本质上是返回一个元组def get_min_max(nums): return min(nums), max(nums) min_val, max_val get_min_max([3, 1, 4, 1, 5])这个“解包”操作在 Python 里非常自然。我写数据处理函数时经常用一个函数返回多个统计量然后一次性赋给多个变量代码直观又紧凑。3.2 没有 return 的函数返回什么前面提到过Python 里没有return语句的函数会隐式返回None。这个概念看似简单实际很容易埋雷。比如你写了一个更新数据库的函数没写return然后调用方用result update_db(...)去接收结果接着用if result:判断是否成功——那这里永远是False因为None的布尔值是假。我建议凡是需要调用方关注结果的函数直接显式return True/False或者返回具体的值不要依赖隐式None。如果函数本身真的只是做动作那也可以保持简洁什么都不返回但函数命名要体现出“做动作而不是给结果”的意图。比如show_plot、send_report别人一看就知道不期待返回值。3.3 返回函数本身闭包的起点前面说的都是返回数据Python 还允许函数把另一个函数作为返回值。这种技巧是闭包和装饰器的地基虽然属于进阶内容但从“代码复用”的角度特别好理解你不只是复用一段逻辑你是在动态“生产”新的逻辑。def make_multiplier(factor): def multiplier(x): return x * factor return multiplier double make_multiplier(2) triple make_multiplier(3) print(double(10)) # 20 print(triple(10)) # 30闭包的本质是内部函数能够记住外部函数作用域里的变量即使外部函数已经执行结束。这个特性在配置化、延迟计算的场景下非常强大。比如根据不同的换算系数动态生成汇率转换函数每产生一个usd_to_cny、usd_to_jpy逻辑都是同一套只是系数不同——代码复用到了“生成函数”这个层面可读性反而更清晰。4. 作用域与生命周期变量去哪儿了4.1 局部变量与全局变量的边界函数内部的变量默认是局部变量只在函数内部生效函数结束后生命周期就结束了。全局变量则是模块顶层定义的变量理论上所有地方都能访问。count 0 # 全局变量 def increment(): count 1 # 这里会报错这个报错非常经典函数内部读取全局变量是可以的但一旦你写了count 1Python 会认为你想创建一个同名的局部变量由于你没有在函数内给它初始化于是先读取它的值时发现还没定义就会抛UnboundLocalError。正确做法是使用global声明count 0 def increment(): global count count 1但global能不用就尽量别用。滥用全局变量会让函数的输入输出变得隐晦你表面上调用了一个函数结果外部状态悄悄变了排查问题的难度直接上升。我也知道某些场景下全局变量确实方便比如脚本里全局配置对象但请记住一个度全局变量是“全局配置”而不是“全局临时存储”。如果一个函数依赖某个全局状态才能运行那最好把状态通过参数显式传进去函数的行为就会变得可预测也更容易测试。4.2 nonlocal嵌套函数的“内层通信”global解决的是函数访问模块级变量的需求nonlocal解决的是嵌套函数访问外层函数变量的需求。看这个例子def outer(): total 0 def inner(): nonlocal total total 1 inner() inner() return total print(outer()) # 2如果没有nonlocal内部函数里执行total 1一样会报错因为无法区分“修改外层变量”和“创建新局部变量”。nonlocal明确告诉 Python这个变量不是本层的去外层作用域找。闭包场景下nonlocal几乎是必需的操作否则内部函数没法更新外部变量。我的实践心得是嵌套函数和闭包很灵活但层级不宜过深。嵌套超过一两层代码阅读成本会显著上升。如果嵌套逻辑复杂优先考虑拆成独立的命名函数用参数传值只有在确实需要“记住环境”的时候才用闭包。5. 代码复用的高阶形态lambda、内置函数与回调函数5.1 lambda 表达式轻量函数但不负责复杂逻辑lambda是 Python 提供的匿名函数写法适用场景是“逻辑非常简单不值得用def定义命名函数”。基本语法square lambda x: x ** 2 print(square(5)) # 25这等价于def square(x): return x ** 2但我不建议你把 lambda 赋值给一个变量——那就失去匿名函数的意义了不如干脆写def。lambda 的真正舞台是我在其他函数内部作为简短的逻辑片段使用比如排序时指定排序规则students [(张三, 85), (李四, 92), (王五, 78)] students.sort(keylambda item: item[1]) print(students) # 按分数升序排序lambda 的坑在于它只能写单个表达式不能写循环、不能写多条语句。一旦逻辑超过两三行可读性会急剧下降我建议强制执行一条规则逻辑复杂就拆成普通函数别硬塞进 lambda。这跟写“越短的代码越好”那种想法恰恰相反——代码是写给人看的不是用来浓缩成谜语的。5.2 内置函数Python 自带的“函数仓库”天天写函数别忽略 Python 自带的一大批内置函数。它们本身就是“官方封装的复用代码”质量和性能都比你手写循环要好。我日常用的最多的几个map(func, iterable)对序列每个元素应用函数返回迭代器。filter(func, iterable)筛选出使函数返回值为真的元素。sorted(iterable, keyfunc)按 key 规则排序比直接手写排序算法可靠得多。enumerate(iterable)同时拿到索引和元素写循环时比用range(len(...))优雅得多。zip(iterable1, iterable2)把多个序列按位置配对常用于批量处理多列数据。配合 lambda可以写出很紧凑的代码scores [78, 92, 85, 60] passed list(filter(lambda x: x 60, scores))filter筛选出及格线以上的分数list再转成列表。这段逻辑如果用for循环写需要三行这里一行搞定可读性也还行。但注意lambda 加上内置函数一旦组合复杂也要警惕过度压缩。如果一个操作链条已经需要写注释才能看懂就该拆步骤了。5.3 回调函数与高阶函数把函数当成“参数”传来传去函数在 Python 里是一等公民也就是函数本身可以作为参数传入另一个函数也可以在函数内部调用它。这个机制最常见的应用就是“回调”——我定义一个函数但是我不直接调用而是把它作为参数传给另一个函数由另一个函数在合适的时机去调用。举个最简单的例子模拟一个“事件触发”def on_success(): print(任务成功) def run_task(callback): print(任务开始) # 模拟处理... callback() print(任务结束) run_task(on_success)run_task只负责流程调度真正执行成功的细节由外部传入的on_success决定。这个思想在爬虫、自动化脚本、GUI 编程里极其常见。比如你在爬虫里写了下载函数download(url)再写一个解析函数parse(html)然后把parse作为回调传给下载流程下载完成自动解析。代码即可复用的边界变得非常清晰底层的“下载→解析”框架不变变化的永远是那一步回调。高阶函数接收函数作为参数或返回函数的函数让“复用”不再局限于“复用一段逻辑”而是“复用一套流程模板”。这也是为什么sorted、map、filter这类内置函数如此强大——你只需要提供 key、条件或变换规则它们负责把整个遍历、筛选、排序的流程跑完。6. 实操从需求到函数的完整落地流程前面讲了不少概念现在把思路完整串一遍给你一个小需求你怎么一步步把它拆成函数。6.1 需求拆解把“重复动作”翻译成“函数蓝图”假设需求是读取一个文本文件中的多行数字计算总和、平均数、最大值、最小值并打印一份统计报告。现在这个需求里哪些动作是重复的哪些动作是独立的可以先做一个简单的分解读取文件并解析出数字列表对列表做统计计算总和、平均、最大、最小格式化输出报告这三个动作是强相关的可以都做成函数。尤其要注意第一个动作和第三个动作是非常典型的封装边界输入输出明确主流程可以直接编排它们。6.2 第一个完整函数代码逐行解析def read_numbers(filepath): numbers [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if line: numbers.append(float(line)) return numbers这里需要解释几个点with open(...) as f是上下文管理器文件用完后自动关闭避免资源泄漏。line.strip()去掉每行首尾的空白字符和换行符。if line跳过空行防止空字符串导致float()转换失败。float(line)把文本转成数值假设文件每行只有一个数字。这个函数封装了“文件读取解析”的逻辑。主流程不关心文件内部格式细节只需要传入一个路径就能得到数字列表。以后如果需求变成“从 CSV 读取”只需改这一个函数统计逻辑完全不用动。6.3 统计与报告把流程拆成可组合的“零件”统计函数def summarize(numbers): total sum(numbers) average total / len(numbers) if numbers else 0 maximum max(numbers) if numbers else 0 minimum min(numbers) if numbers else 0 return total, average, maximum, minimum这里有个细节len(numbers)如果为 0直接除会抛ZeroDivisionError所以用三元表达式做了空列表保护。这类“边界值处理”是写函数时最容易忽略的而恰恰是线上最容易炸的地方。多想想“如果输入是空、如果是单一元素、如果是畸形数据”函数健壮性会高出不少。报告函数def print_report(filepath): nums read_numbers(filepath) total, avg, maximum, minimum summarize(nums) print(f文件: {filepath}) print(f数量: {len(nums)}) print(f总和: {total}) print(f平均: {avg}) print(f最大: {maximum}) print(f最小: {minimum}) if __name__ __main__: print_report(data.txt)if __name__ __main__:这个写法刚入门可能看不懂它的意思是只有直接运行当前脚本时才执行这里的代码。如果这个文件被其他文件导入这些调用不会自动执行——这就是模块化复用的基础。把可复用的函数和“仅当主程序运行时才执行的入口代码”分开是做代码复用的关键一步。这样别人import你的模块时不会莫名其妙触发一堆副作用。整个流程下来主函数print_report只做三件事调用读文件函数、调用统计函数、输出结果。每一件事的具体细节被隔离到各自的小函数里任何一个环节需求变化只改对应位置的函数即可。这就是函数式拆解的威力。7. 常见问题与排查技巧实录7.1 环境问题命令行报“无法识别 pip/npm”新手最容易卡住的往往不是语法而是环境配置。比如在 PowerShell 里执行pip install requests报错“无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”本质原因是 pip 的可执行文件所在目录没有被加入系统 PATH。解决方法分两步找到 Python 安装目录下的Scripts子目录例如C:\Python312\Scripts。把它和 Python 安装目录一起加入系统环境变量 Path。改完之后要新开一个终端窗口环境变量才会重新加载。如果依然不行还可以试试python -m pip install requests这种方式直接通过 Python 模块机制调用 pip不依赖 PATH 里有没有 pip是一个很稳的绕行方案。同样python命令本身报“无法识别”大概率是安装时没勾选“Add Python to PATH”。卸载重装时勾上或手动配置 Path 都行。这类问题虽然简单但几乎每个学 Python 的朋友都经历过一遍网上相关搜索常年霸榜。所以别觉得自己很蠢大家都一样。7.2 函数作用域问题改了全局变量怎么没反应前面讲global时提到一个现象函数内部对同名变量赋值会被 Python 视为创建局部变量。常见的报错是UnboundLocalError: local variable count referenced before assignment。遇到这个问题先看函数内部有没有对全局变量做赋值操作。如果有且确实想修改全局变量加global声明但如果只是想读取并不需要声明。还有一个容易忽略的情形函数内部的、-这类操作也算“赋值”同样会触发局部变量判断所以视情况需要加global。我更推荐的做法是把这种“依赖外部状态才能运行”的函数改成“把外部状态作为传入参数、返回值带回更新结果”的纯函数风格。比如count 0 def increment(count): return count 1 count increment(count)这样函数无副作用运行多少次都一样你随时可以通过参数预测结果排查问题的成本低很多。7.3 默认参数共享列表调用两次结果越来越长前面举过students[]的例子。如果发现某个函数每次调用结果列表里都会包含上一次调用的数据十有八九就是可变默认参数“残害”的结果。排查方法很直接在函数内部加一行打印看看默认参数的id是否每次都相同。如果相同说明你正共享同一个对象。我还是建议彻底规避这种写法任何默认参数都不用可变对象。列表用None、字典用None然后在函数内先判断、再初始化。这是刻进肌肉记忆的习惯一个小改变能省掉大量隐蔽 bug。7.4 调试技巧打印、断点与小步验证排查函数问题第一反应别是猜要有手段。最简单的方案是print()——在函数入口、退出、关键分支各打一条日志。比如def process_data(data): print(f[DEBUG] 参数长度: {len(data)}) ... print(f[DEBUG] 返回结果前的最值: {max(data)})如果代码比较复杂光靠print会明显拖慢节奏可以直接用 IDE 的断点调试。在 PyCharm 或 VS Code 里点到行号旁边出现红点然后 Debug 运行程序会停在红点那一行你可以逐步查看此时各个变量的值一步步定位问题。VS Code 的 Python 环境配置比较容易被新手卡住装好 Python 插件后确保选择了解释器CtrlShiftP 搜 “Python: Select Interpreter”选到自己安装的那个 Python 版本就行。还有一个我的个人习惯写函数时先写一小段测试调用马上验证输出是否符合预期再继续往下写。很多人喜欢一次性写完整套逻辑再运行结果报错时不知道是输入读错了还是中间计算错还是输出写错了。改成“每写一个函数立刻用小数据试跑一次”调试成本会大幅下降。7.5 关于常见的字节码残留问题有时你改了函数代码但是运行结果还是旧版逻辑怎么排查都不对。先做两件事检查是不是运行了同一个文件因为工作目录不同可能导致导入的是其他路径下的同名模块再检查是不是 Python 生成了旧的__pycache__缓存尤其是跨项目复制代码时容易发生。缓存冲突通常可以通过删除该目录或者重启解释器解决。这类问题不属于函数语法问题但确实会浪费很多时间值得记一笔。8. 关于代码复用的一点个人体会函数学到这里你会发现一个很有意思的转变刚开始写代码脑子里全是“怎么让这段逻辑跑通”学会函数之后脑子里会多一个问题——“这段逻辑是不是可以被拆出来下次换个参数还能用”这个多出来的问题其实就是“代码复用”的起点。它不光是省键盘功夫更重要的是它把代码变成了可组织、可测试、可进化的结构。我印象很深的一次重构一个数据清洗脚本原本有 600 多行里面有大量重复的空值处理、类型转换、字段格式统一逻辑。我把这些逻辑提取成十几个小函数每个函数只负责一件事主流程就变成了一行行清晰的调度。那之后需求变更时我只需要找到对应的小函数改测试也只针对改动的函数跑一遍整个人的心理负担轻了很多。代码没有变少太多但维护成本天差地别。回到函数本身我想建议每位刚开始接触 Python 的朋友别急着学高深的语法技巧先把函数用“笨”一点的方式练扎实——给你的每个重复动作命名给你的每个过程定义明确的输入和输出给你的函数起一个诚实、清晰的名字。做到这三点之后再来研究 lambda、闭包、装饰器这些进阶玩法你会发现新姿势并不难理解因为本质都是同一件事把可以复用的逻辑封装得更加优雅。最后分享一个小技巧别把函数一次写太多参数。如果一个函数有六七个参数调用时你很难记得住顺序和含义这时候就该把相关参数打包成一个字典或数据类传进去了。函数接口设计得简单它才是真的好复用接口复杂到别人得翻文档才能调复用的价值就打了大折扣。代码复用不是靠堆代码量恰恰相反是靠把边界划得干净利落。