1. 从一段报错开始为什么要专门学函数先聊个场景。你在终端里敲了一行pip install requests结果跳出来一串红字“无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称。”或者更离谱的你想装 Node 包把npm敲成了nmp同样被系统怼回来。这种时候大部分人第一反应是“环境坏了”但静下心想想问题往往出在你没搞懂“命令、程序、函数、脚本”这几个概念之间的关系。Python 的函数学习笔记本质上就是在回答一类问题代码里的逻辑该怎么组织、命名、复用才能让一个脚本从“写一次跑一次”变成“写一次跑无数次”。函数就是把一段会反复用到的逻辑装进一个盒子里给它起个好记的名字需要的时候喊一声就行。这篇笔记不是什么高深的源码剖析而是把函数从定义、参数、作用域、内置函数、回调、到环境配置和实战踩坑整条线捋一遍适合刚把 Python 装好、准备认认真真学语法的初学者也适合那些写过一段时间脚本但总觉得代码“乱糟糟”的兄弟。我不打算上来就丢一长串语法表。那样你背完就忘。我们换个方式先搞懂函数存在的意义再动手写、动手拆、动手踩坑。等你把这篇笔记里的代码都跑过一遍再回头看到pip报错大概率就能自己判断是不是 PATH 环境变量的问题了。2. 函数的骨架定义、调用、参数、返回值2.1 函数定义与调用的基本规则Python 里定义一个函数靠def关键字。最简单的写法长这样def greet(name): return Hello, name msg greet(小明) print(msg)这背后有四个点值得抠清楚。第一函数名说白了就是一个变量名。它指向一块内存里面存着你的函数对象。你调用greet(小明)就相当于告诉解释器去执行greet指向的那段代码参数是小明。理解了这一点后面讲回调函数、讲lambda、讲把函数当参数传递你都不会觉得玄乎。第二缩进是 Python 的生命线。函数体必须统一缩进你写def greet(name):之后下一行按 Tab 或者四个空格然后整个函数体保持在同一个缩进级别。很多人报IndentationError就是 Tab 和空格混用了。我自己的习惯是编辑器一律设置为“Tab 转 4 空格”从源头掐死这个问题。第三return不是必须的。如果函数体内没有returnPython 会默认返回None。这个“隐式返回 None”的行为经常让新手在调试时一脸懵明明函数执行了为什么变量是None其实不是你写错了是忘了写return。第四函数定义不会执行函数体。def只是把代码存起来到调用那一行才真正运行。所以你在文件底部放了一堆def没有调用程序跑完什么输出都没有这是正常的。关于“函数声明”这个词有些从 JavaScript 转过来的同学会问Python 有没有函数声明和函数表达式的区别Python 更简单def就是声明加定义一体不存在“先声明后赋值”这种操作。你在一个模块里先定义后调用即可不需要像 C 语言那样写头文件。2.2 参数传递别被“传值还是传引用”绕晕这是函数学习里最容易把人绕晕的一个话题我直接给你结论不可变对象整数、字符串、元组作为参数传入时函数内部修改它不会影响外部变量。因为 Python 里的整数、字符串都是不可变的你x x 1实际上是创建了一个新对象把局部变量x重新指向了新对象外部那个变量压根没动。可变对象列表、字典、集合作为参数传入时函数内部修改它的内容外部变量会跟着变。因为列表本身是个可变容器你lst.append(1)是在原地修改同一个对象。听起来还是有点绕我拆个例子def try_change(a, b): a a 10 # 重新绑定局部变量 a外部不感知 b.append(99) # 原地修改列表外部感知 x 5 y [1, 2, 3] try_change(x, y) print(x) # 5没变 print(y) # [1, 2, 3, 99]变了这个例子基本涵盖了面试里 80% 的“闭包/引用”题目。你要记的核心不是“值传递”或“引用传递”这个名词而是记住一条原则赋值操作改变的是局部变量的指向原地修改操作改变的是对象本身。再补充一个经典坑默认参数不要用可变对象。看这段代码def add_item(item, container[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]惊不惊喜默认参数container[]只会在函数定义时被创建一次后续每次调用如果没传container用的都是同一个列表对象。所以第二次调用时列表里还留着上次的1。正确写法是def add_item(item, containerNone):然后在函数体里if container is None: container []。2.3 不定长参数*args和**kwargs到底干什么用有时候你写一个函数不希望限制参数的个数或者希望调用者可以自由传关键字参数。这时候就该*args和**kwargs登场了。def log(level, *args, **kwargs): print(Level:, level) print(Args:, args) # 元组包含所有多余的位置参数 print(Kwargs:, kwargs) # 字典包含所有多余的关键字参数 log(INFO, user login, ip127.0.0.1, retry3)运行结果里args会收到(user login, ip127.0.0.1)kwargs会收到{retry: 3}。星号在这里的作用是把多个参数打包成一个元组或字典反过来在调用时你还可以用*和**来解包def calc(a, b, c): return a b c nums [1, 2, 3] print(calc(*nums)) # 等价于 calc(1, 2, 3) data {a: 10, b: 20, c: 30} print(calc(**data)) # 等价于 calc(a10, b20, c30)这个语法在日常开发里最常见的使用场景有两个。一个是你封装一个通用装饰器需要把任意参数转发给被装饰的函数另一个是你写一个接口函数希望接受各种配置项而不需要预先定义全部参数。理解了这两个场景你对*args和**kwargs就算真正入脑了而不是停留在“见过但不知道怎么用”的状态。3. 内置函数与高阶技巧别重复造轮子也别忘了函数是一等公民3.1 高频内置函数abs、sqrt、sum 这些背后的共同逻辑Python 自带了一批很实用的内置函数比如abs取绝对值、len取长度、sum求和、max/min取最值、round四舍五入。这些函数本身很简单但我想强调的是内置函数通常比你自己用循环实现快得多因为它们在解释器底层做过优化。sqrt不在内置函数列表里它在math模块中需要import math或者from math import sqrt。很多人刚学时纠结为什么abs可以直接用sqrt必须导入原因是abs是语言层面的、超级高频的基本操作所以常驻内置命名空间sqrt属于数学库的范畴Python 的设计哲学是“核心尽量精简功能按模块划分”。所以当你看到某个函数没法直接用第一反应应该是“这个函数可能在某个模块里”而不是怀疑自己安装有问题。再扩展一个思路Excel 里的SUMPRODUCT函数在 Python 里等价于sum(x * y for x, y in zip(list1, list2))。两者的核心思想都是“把两个序列按位相乘再求和”。这类函数思维其实是跨工具的你学 Python 函数时如果能把 Excel 里的公式逻辑、SQL 里的聚合函数拉通对比理解会快很多——它们本质上都是输入一堆数据输出一个结果区别只在语法外壳。3.2 把函数当作值传递回调函数与高阶函数Python 里函数是一等公民意思是函数可以像整数、字符串一样被赋值给变量、放进列表、作为参数传进另一个函数、作为返回值返回。这里最直观的例子是回调函数。想象你在写一个下载工具下载完成后要执行不同的后续动作有的要发通知有的要转格式有的要入库。如果你把“后续动作”作为参数传进去代码会非常优雅def download(url, on_success, on_failure): try: data fetch_data(url) on_success(data) except Exception as e: on_failure(e) def send_notify(data): print(下载完成发送通知长度:, len(data)) download(https://example.com/file.zip, send_notify, print)你注意到没有send_notify和print都是直接传的函数名没有加括号。这就是关键加括号表示调用不加括号表示把函数本身传过去。很多人在研究“回调”时犯的错就是把这两者混了。内置函数sorted也支持回调式的参数key。比如按字符串长度排序words [pear, apple, kiwi, blueberry] words_sorted sorted(words, keylen) print(words_sorted) # [kiwi, pear, apple, blueberry]这里的keylen就是传了一个函数给sortedsorted 内部会对每个元素调用len获取排序键。这种“传函数进去”的写法就是高阶函数的基本形态。高频使用的还有map、filter、functools.reduce不过 Python 社区现在更推荐用推导式表达可读性更好numbers [1, 2, 3, 4, 5] # 不推荐map lambda可读性一般 squares list(map(lambda x: x ** 2, numbers)) # 推荐列表推导式直白 squares [x ** 2 for x in numbers]3.3 匿名函数 lambda用完即走的轻量函数听到 JS 里的“箭头函数写法”很多 Python 新手会好奇它在 Python 里的对应物。有就是lambda。一个典型写法是add lambda x, y: x y print(add(2, 3))但我想劝一句给 lambda 起名字是最没必要的用法。既然你打算多次使用为什么不用def定义一个带名字的函数呢lambda的价值在于它短小到不值得单独起名只在某个地方用一次。比如你要排序一个字典列表students [ {name: Tom, score: 82}, {name: Jerry, score: 95}, {name: Alice, score: 78}, ] students.sort(keylambda s: s[score]) print(students)keylambda s: s[score]这个写法很自然因为逻辑就是一行不值得单独def一个函数。反过来如果排序逻辑复杂到需要写 if 分支那就应该用普通函数加 if 语句不要硬塞进 lambda 里。lambda 只能写一个表达式不能写赋值语句、不能写多行逻辑这是它最大的限制。4. 环境配置与报错排查函数写对了跑不起来是怎么回事4.1 把 Python 装明白PATH、解释器和 venv很多人在“函数学习”上投入了大量时间结果被环境问题卡住了半天特别劝退。而环境问题里十有八九出在 PATH 和解释器选择上。Windows 用户安装 Python 时有一个最容易忽略的步骤安装器第一页最下方的Add Python to PATH必须勾上。如果没勾你在终端里敲python就会得到“无法将‘python’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。同理pip报错、npm报错、nmp报错它们本质上是同一类问题——可执行文件的路径没有被操作系统登记。遇到这类报错排查路径我建议按顺序来先确认你装没装这个程序。比如pip报错你得先运行python -m pip --version如果这个能用说明 pip 已经装了只是它的可执行脚本路径不在 PATH 里。以后直接用python -m pip就好不必非依赖pip这个命令。检查 PATH 环境变量里有没有包含python.exe所在目录和它的Scripts目录。Windows 环境变量编辑界面里新增两条C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\和C:\Users\你的用户名\AppData\Local\Programs\Python\Python311\Scripts\注意版本号要换成你实际的。命令拼写错误。nmp install nmp这种怎么看怎么不对。npm是 Node.js 的包管理器不是 Python 的东西如果你没装 Node 就去敲npm当然会报错。这类报错里最冤枉的就是把npm敲成nmp或者把pip敲成pmp手指一抖系统翻脸不认人。还有一个我强烈建议配合函数学习一起做的配置用虚拟环境 venv。在项目目录下运行python -m venv venv然后激活它再装包。这样你每个项目的依赖互不干扰也不会出现“A 项目用的 pandas 2.0B 项目用的 pandas 1.5放在一起互相打架”的局面。4.2 VSCode / PyCharm 里怎么配置 Python 环境写函数不需要太重的 IDE但你需要一个能让你看清缩进、快捷补全、即时报错的编辑器。VSCode 和 PyCharm 是 Python 社区最主流的两款选哪个完全看个人习惯。用 VSCode 的话三步安装官方 Python 扩展、打开命令面板、选择解释器。有个频繁踩坑的点是你明明安装了 Python 扩展但运行时发现用的还是其他解释器或者 CtrlF5 跑起来根本找不到pandas模块。这时候打开左下角点 Python 版本号把解释器切到你虚拟环境里那个python.exe就行。不切好你的函数写得再漂亮也白搭因为模块搜索路径压根没指向你的项目解释器。用 PyCharm 的话关键在新建项目时选择“Existing interpreter”或创建虚拟环境。PyCharm 的图形界面让解释器切换很直观新手更容易操作。它有个小毛病是默认缓存索引比较大第一次启动慢一点但无伤大雅。还有一点想提醒网上很多教程让你“一键安装 Anaconda”。如果只是学函数基础我其实不建议一上来就上 Anaconda因为它自带的 conda 环境管理和 Python 原生 venv 不是一回事初学者容易混淆。用官方 Python 安装包加 venv路径清晰理解也朴素等你需要搞数据科学再上 Anaconda那时你对环境已经心里有数了。4.3 报错信息速查见招拆招的常见场景我把函数学习阶段最容易碰到的几类报错列成一个表方便你直接对号入座报错信息常见原因处理办法SyntaxError: invalid syntax漏了冒号、括号不匹配、混用 Tab 和空格从报错行上一行开始看往往问题出在前面那一行IndentationError: unexpected indent多余缩进某行不该缩进却缩进了检查前后缩进层级是否一致NameError: name xxx is not defined函数名或变量拼写错误或定义在调用之后但没被读取到检查拼写确认定义确实执行过TypeError: greet() missing 1 required positional argument: name调用函数时参数个数不对数一数参数要么补上要么给默认值UnboundLocalError: local variable x referenced before assignment函数内部给变量赋值前就引用了它Python 认为这个变量是局部的如果确实想用全局变量用global x声明但更推荐改成传参return返回了None忘了写 return或 return 写在分支里却没走到检查所有分支是否都有 return这个表基本涵盖了新手跑函数时的 90% 报错。你别试图背下来只需要知道报错信息里的关键词指向哪一类问题再回来对照处理就行。5. 函数在真实项目里的落地从玩具脚本到工程代码5.1 量化交易策略里的函数封装把每一步逻辑拆开很多人搜“python量化交易策略代码”本质上是想看函数如何被组织在一个真实场景里。我们拿一个简单的双均线策略举例。策略逻辑不复杂当短期均线上穿长期均线时买入下穿时卖出。你可以把所有逻辑都塞在一个几十行的脚本里但更工程化的做法是拆成函数def compute_ma(close_prices, window): 计算移动平均线window 是窗口大小 if len(close_prices) window: raise ValueError(价格序列长度小于窗口大小) return close_prices.rolling(window).mean() def generate_signal(short_ma, long_ma): 返回 1 表示买入信号-1 表示卖出信号0 表示无信号 # 判断金叉死叉用前一个值和当前值比较 if short_ma.iloc[-2] long_ma.iloc[-2] and short_ma.iloc[-1] long_ma.iloc[-1]: return 1 if short_ma.iloc[-2] long_ma.iloc[-2] and short_ma.iloc[-1] long_ma.iloc[-1]: return -1 return 0 def run_backtest(df, short_window5, long_window20): 主回测入口输入 DataFrame返回每次交易的信号序列 close_prices df[close] short_ma compute_ma(close_prices, short_window) long_ma compute_ma(close_prices, long_window) signals [] for i in range(long_window, len(df)): signals.append(generate_signal(short_ma.iloc[:i1], long_ma.iloc[:i1])) return signals这样拆的好处你一眼就能看出来compute_ma只顾算均线generate_signal只判断信号run_backtest只负责把前两个串起来。后面如果要改成“三均线”策略你只需要加一个参数和一行判断不需要动整个脚本。这种“单一职责”的函数设计就是工程代码和玩具脚本的核心区别。这里也顺便解释一下“闭包”的概念在策略代码里的体现generate_signal内部虽然只引用了short_ma和long_ma但在数据切片场景下你传入的其实是 DataFrame 的一段视角它引用的是原始数据对象的一部分。理解可变对象与引用的作用域规则对调试这类策略循环很有帮助。5.2 深度学习里的损失函数函数思维在模型训练中的体现搜“yolo损失函数”的人可能是在查目标检测模型训练代码。实际上不管是 YOLO 的损失函数还是 PyTorch 里的自定义损失函数它的本质依然是一个函数输入是预测值和真实标签输出是一个标量损失值。PyTorch 里定义自定义损失函数可以直接写成纯函数def custom_loss(pred, target): 简单的均方误差损失仅用于示例 return ((pred - target) ** 2).mean()也可以继承nn.Module把它封装成一个类但底层仍然是“输入数据、输出损失”。你学 deep learning 时如果只盯着网络结构而忽略了损失函数也是一个普通函数你会发现很多调参操作都无从下手。比如你在训练时打印 loss 出来发现始终不下降这时候你就需要去检查你写的损失函数内部逻辑是否合理、梯度是否传回。这里用到的技能和你排查一个普通 Python 函数的返回值逻辑本质上没有任何区别。这也引出一个更大的观点函数思维是一种跨领域的元能力。你在 Python 里写一个compute_ma和你在 Excel 里写一个公式、在 SQL 里写一个聚合查询底层都是同一个心智模型——输入、处理、输出、复用、组合。只不过 Python 给了你最自由的表达方式。5.3 装饰器函数的函数学完函数基础后你迟早会碰到装饰器。装饰器本身就是一个函数它接收一个函数作为参数返回一个增强后的函数。简化的例子def log_call(func): def wrapper(*args, **kwargs): print(fCalling {func.__name__}) result func(*args, **kwargs) print(fFinished {func.__name__}) return result return wrapper log_call def add(a, b): return a b print(add(1, 2))这段代码里log_call就是把add函数传给log_call再把返回的wrapper重新绑定到名字add上。理解了“函数是对象、函数可以返回函数”这两个前提装饰器就不再是什么黑魔法而只是一层语法糖。装饰器在项目里最常见的用途是日志记录、权限校验、耗时统计。比如你想统计一个函数的运行时长又不想在每个函数里手写time.time()的样板代码封装一个time_cost装饰器就非常合适。这种“不修改原函数却给原函数增加功能”的能力是函数思维里比较高级的体现也是很多人从入门跨到进阶的转折点。6. 我的几条实战经验以及对函数学习的建议写到这里我不打算再堆语法点了。最后分享一些我在带新人、包括自己早期学习时反复遇到的真问题。第一先别碰太多奇技淫巧。你可能在 B 站上看到有人用一行代码实现斐波那契数列或者把装饰器玩出花来。很帅但不适合你当前阶段。你真正需要的是把函数定义、参数传递、返回值、作用域这些基础焊死。基础动作扎实了后面看高级技巧就是一层窗户纸。第二刻意练习“函数拆解”。拿到任何一个稍微复杂的需求先别急着写代码先拿笔在纸上画一画这个需求可以拆成哪几个子任务哪些子任务会被复用哪些子任务适合合并成一个函数比如你写一个爬虫脚本拆成fetch_page、parse_html、save_to_file三个函数比把几十行代码一路写到底要好调试得多。函数拆解得越自然报错定位越容易。第三多读报错信息别害怕英文。很多人一看到红色报错就慌直接把错误信息复制到搜索引擎。但排查问题的关键在于“定位问题出在哪个函数、哪一行”。Python 的 traceback 已经把函数调用链按顺序列出来了从下往上看第一个出现在你自己代码里的帧就是问题源头。你没有理由看不懂“line 15, in greet”这种信息。第四建立自己的代码笔记本。学函数的阶段值得记录的不是语法是你踩过的坑。比如我在默认参数上吃过亏在UnboundLocalError上迷惑过在 pip 环境变量上报错过。把这些记下来比收藏一百个别人的教程都有用。等你从新手成长为老手回看这本“避坑史”会很有成就感。函数学习这件事我觉得真正难的不是语法而是建立“把代码组织成函数”的习惯。语法是死的查一下就会习惯是活的要靠每天写代码慢慢养。如果你刚学完这段回头把你以前写的脚本重新看一遍想想哪些地方可以抽成函数、哪些地方可以并入内置函数、哪些地方参数设计太啰嗦这一轮重构下来比再看十篇笔记都管用。