第 6 课 把重复计算整理成函数,并学会从报错里找线索¶
上一课的相邻收益会被使用很多次。函数把一组步骤保存为有名字的小工具,模块和包负责组织更多工具,报错则告诉我们哪一项约定没有满足。
同一条公式为什么不该复制很多遍¶
相邻收益率公式会出现在数据检查、指标计算和回测账本中。若每次都重新写 new_price / old_price - 1,某一处可能忘记减 1,另一处又可能把前后价格写反。复制越多,修改和复查越困难。
Python 的函数会把一组步骤保存为一个名字。函数先规定需要哪些输入,内部完成计算,再把结果交回调用它的代码。以后每次使用同一规则,只需提供新的输入,无需复制实现细节。
开始写函数以前,先用自然语言确定约定。本课函数接收旧价格和新价格,两个输入都应该是大于零的数值,输出为小数形式的简单收益率。旧价格为 10 元、新价格为 10.5 元时,答案应该是 0.05。
def 表示开始定义函数,simple_return 是函数名,括号中的 old_price 和 new_price 是两个参数。冒号以后进入函数内部,缩进的第一行完成计算,return 把结果交给外部。运行这段定义只会保存步骤,不会自动计算收益。
调用函数时要写名字和括号,并在括号中提供具体输入。下面第一次使用位置参数,Python 按先后顺序把 10 交给旧价格、10.5 交给新价格。第二次使用关键字参数,名字直接说明每个数值扮演什么角色。
rate_a = simple_return(10, 10.5)
rate_b = simple_return(old_price=20, new_price=19)
print(rate_a)
print(rate_b)
输出应为 0.05 和负 0.05。函数只定义一次,却完成了两组独立计算。关键字写法稍长,前后参数容易混淆时更安全;位置写法更紧凑,读者必须熟悉函数约定。
flowchart LR
accTitle: 一次函数调用怎样经过输入、计算和返回
accDescr: 调用者把旧价格和新价格交给参数,函数内部计算收益率,return 再把结果交回外部变量
a[调用函数<br/>simple_return] --> b[输入参数<br/>旧价格与新价格]
b --> c[函数内部<br/>执行收益率公式]
c --> d[return<br/>交回计算结果]
d --> e[外部变量<br/>继续保存或计算]
参数、局部变量和返回值有什么区别¶
参数是函数开始执行时收到的信息。局部变量只在函数内部暂时保存中间结果,返回值是函数结束时交出的结果。上一个例子中的 old_price 与 new_price 是参数,result 是局部变量,return result 交出的数值是返回值。
局部变量帮助函数隔离内部细节。外部代码通常不能直接读取函数里的 result,这样不同函数就不容易误改彼此的临时状态。调用者只需要知道输入约定与输出含义,内部实现以后可以增加费用或检查,只要约定保持清楚,其他代码仍能使用。
若函数只使用 print 显示结果,却没有写 return,外部变量通常会得到 None。打印适合让人临时观察,返回值适合交给后面的程序继续处理。下面函数能够在屏幕上显示 0.05,但 saved 保存的不是这个数字。
def show_return(old_price, new_price):
print(new_price / old_price - 1)
saved = show_return(10, 10.5)
print(saved)
运行时先显示 0.05,随后显示 None。函数没有明确交回结果,Python 用 None 表示“没有返回值”。研究代码通常让计算函数返回数值或表格,再由报告部分决定怎样打印和画图,计算与展示因此更容易分别测试。
输入约定为什么要在函数内部检查¶
旧价格为零时,收益率分母无效;旧价格为负数时,普通股票价格也没有合理含义。若函数直接计算,零会产生除零错误,负数则可能安静地得到一个数字。更稳妥的函数会在计算以前检查输入,并在约定不满足时主动停止。
def simple_return(old_price, new_price):
if old_price <= 0:
raise ValueError("old_price 必须大于 0")
if new_price < 0:
raise ValueError("new_price 不能小于 0")
return new_price / old_price - 1
raise 会主动产生错误,ValueError 表示数据类型允许当前操作,但具体取值不符合约定。错误文字应告诉读者哪个输入有问题以及合理范围。函数收到旧价格零时,会在除法以前停止,报错位置也更接近原因。
输入检查不能替代数据研究。停牌、退市和期货负价格事件可能需要不同业务规则,函数不能把所有复杂情况都压成“大于零”。教学函数只处理普通正价格,适用范围要写在说明中。真实项目应根据资产类别建立更准确的约定。
文档字符串怎样把约定留在代码旁边¶
函数定义下面可以加入三引号文字,这段内容叫文档字符串。它应简要说明函数作用、输入单位、返回值和重要限制。编辑器与 help() 可以读取它,几个月以后重新打开项目时也不必到别处猜测。
def simple_return(old_price, new_price):
"""计算两个正价格之间的简单收益率,返回小数形式。"""
if old_price <= 0:
raise ValueError("old_price 必须大于 0")
return new_price / old_price - 1
这一句没有描述公式的每个符号,因为代码已经清楚展示了计算。它补充的是函数用途、价格条件和输出形式。复杂函数还可以逐项写参数和异常,入门阶段先养成留下约定的习惯。
小测试怎样证明几个已知场景¶
函数写完以后,应先使用答案已知的小例子。价格上涨、不变和下跌分别覆盖三种基本结果。assert 会检查左边是否与预期相等,条件失败时立即停止。
assert abs(simple_return(10, 11) - 0.1) < 1e-9
assert simple_return(10, 10) == 0
assert abs(simple_return(10, 9) + 0.1) < 1e-9
第一条与第三条使用容许误差,避免浮点尾数影响严格相等判断。第二条的结果恰好为零,可以直接比较。三条测试分别证明函数在这三个输入上符合预期,没有证明所有市场数据都正确。还应检查非法输入是否真的报错,未来课程会使用更完整的测试工具。
测试的价值来自先写预期。只运行函数再观察一个看起来合理的数字,很容易接受错误结果。手算 10 到 11 的收益率以后,程序必须接近 0.1;若得到 10%,显示格式可能正确,内部值仍要确认是否为 0.1。
一个文件为什么叫模块¶
函数增加以后,把所有内容放在同一个脚本会越来越长。Python 把一个 .py 文件视为一个模块。可以把稳定的收益计算保存在 calculations.py,其他文件再通过 import 使用它。
第一行让当前文件从 calculations 模块中取出 simple_return 这个名字,第二行才真正调用函数。导入不会把函数来源变得可靠,研究者仍要知道模块版本和函数约定。它只负责让不同文件共享同一份实现。
模块可以按照职责分开。读取数据、计算指标、生成信号、维护账本和画报告承担不同任务,分别组织以后更容易单独检查。文件数量也不能盲目增加,一个模块最好围绕一类稳定职责。
data.py 只处理数据读取与清洗,indicators.py 保存指标计算,strategy.py 描述信号,backtest.py 维护成交与账本,report.py 读取结果并画图。策略函数不应偷偷下载数据,画图函数也不应在内部修改持仓,这些边界能减少难以追踪的副作用。
包、安装和导入分别发生了什么¶
多个相关模块可以组成包。NumPy、pandas 和 Matplotlib 都是别人维护的第三方包。它们包含许多文件、版本和依赖,安装以后才出现在某个 Python 环境中。
安装把包文件放入当前环境,导入让当前代码使用包里的名字,调用让某个函数或方法真正执行。三个动作常被初学者混在一起,所以程序提示“找不到 pandas”时,要先判断当前环境是否安装,再判断代码是否正确导入。
| 动作 | 发生的事情 | 常见检查 |
|---|---|---|
| 安装 | 把指定版本的包放进 Python 环境 | 当前使用的是哪个 Python |
| 导入 | 在当前文件中取得包或函数名称 | 名称与拼写是否正确 |
| 调用 | 给函数输入并执行步骤 | 参数和返回值是否符合约定 |
import pandas as pd 会导入 pandas,并在当前文件中用 pd 作为简称。简称来自社区长期习惯,大量教程都会采用,保留它更容易阅读资料。简称只在当前代码中有效,不会改变包的正式名称。
浏览器练习场已经提供本书示例所需工具,Python 入门阶段无需安装本地环境。完成第 8 课以后,想保存完整项目的读者再按安装页创建独立环境。延后安装可以先把注意力放在代码含义,无法联网时仍能继续阅读手算示例。
报错为什么要从最下面读起¶
Python 报错通常包含一段调用轨迹,也叫 traceback。上方记录程序经过哪些文件和函数,最末行写错误类型与简短原因。初学时先读最后一行,再向上找到第一处属于自己文件的代码,通常能最快接近问题。
File "study.py", line 4, in <module>
rate = new_price / old_prcie - 1
NameError: name 'old_prcie' is not defined
最末行告诉我们错误类型为 NameError,并指出名字 old_prcie 没有定义。向上一行看,代码把 old_price 拼成了 old_prcie。修正拼写以后应重新运行原例子,并确认输出与手算一致。报错消失只能说明这次执行没有在同一位置停止,逻辑结果仍要检查。
| 错误类型 | 常见原因 | 第一项检查 |
|---|---|---|
NameError |
名字未定义、拼错或执行顺序不对 | 对照变量定义与拼写 |
TypeError |
当前类型不支持某项操作 | 打印 type(...) |
IndexError |
列表位置超出有效范围 | 比较索引与 len(...) |
KeyError |
字典或表格中没有这个键 | 打印真实字段名称 |
ValueError |
类型允许操作,具体取值不符合要求 | 检查输入内容和范围 |
同一种错误也可能有不同原因。KeyError: 'close' 可能来自大小写、前后空格、读取失败或数据供应商改名。先打印 columns 或 keys(),再依据数据说明判断,不能只把代码中的名称改成碰巧存在的列。
最小复现怎样缩小问题¶
一百行程序报错时,同时修改十处会失去因果。更可靠的做法是保留能够重现错误的最少数据和代码,这个小例子叫最小复现。下面两行已经足以说明索引 2 超出两项列表范围。
问题缩小以后,可以明确列出有效索引为 0 和 1,修正方案也容易验证。若最小例子没有复现原错误,说明问题可能来自前面的数据处理、环境或函数调用关系,应该逐步加入必要部分,直到错误重新出现。
向 AI 求助时,最小复现同样重要。提供相关代码、完整报错、一个脱敏输入样例和预期结果,请 AI 先解释错误类型与候选原因,再给出一处最小修改。直接要求重写整个文件会带来很多新变化,原原因反而更难确认。
一套可以反复使用的排错顺序¶
遇到错误时先保留完整报错,不要只截最后几个字。随后确认当前运行的是哪个文件和哪套数据,再从末行读错误类型,向上找到自己的最近一行。打印关键变量的类型、长度、列名与前几项,最后缩小成最小例子。
修改时一次只处理一个明确原因,并重新运行原测试。若代码不再报错,还要比较手算结果、输入行数和关键中间值。报错属于显性失败,时间错位、单位错误和未来数据泄漏往往不会报错,因此结果检查比消灭红字更重要。
容易弄错的地方¶
| 现象 | 原因 | 下一步 |
|---|---|---|
函数打印出数字,外部变量却是 None |
函数没有用 return 交回结果 |
明确返回值并再次调用 |
| 参数前后顺序写反 | 位置参数依赖函数定义顺序 | 使用清楚名字或关键字参数 |
| 包已安装,程序仍找不到 | 安装和运行可能使用不同 Python 环境 | 确认当前解释器与安装位置 |
| 报错很长,找不到重点 | 调用轨迹记录了多层经过 | 先读最末行,再向上找自己文件 |
| 改很多处以后报错消失 | 无法判断哪项修改有效 | 回到最小复现,一次只改一个原因 |
| 函数测试通过便相信全部数据 | 小测试只覆盖已写场景 | 再检查时间、单位、缺失与边界 |
一分钟自测¶
函数定义以后会立刻执行吗?print 与 return 对外部代码有什么不同?
查看答案
定义只把步骤保存下来,需要写函数名和括号才会执行。`print` 把内容显示给人,`return` 把结果交回调用者,外部变量才能继续保存和计算。本课术语¶
| 词语 | 现在应该怎样理解 |
|---|---|
| 函数 | 有名字、可重复调用的一组步骤 |
| 参数 | 函数开始执行时收到的输入 |
| 局部变量 | 只在函数内部暂时使用的名字 |
| 返回值 | 函数完成后通过 return 交回的结果 |
| 模块 | 可以被其他文件导入的 Python 文件 |
| 包 | 组织多个模块并带有版本的工具集合 |
| 异常 | 程序无法按当前约定继续时产生的错误对象 |
| 调用轨迹 | 报错中记录文件与函数经过的路径 |
| 最小复现 | 能够稳定重现问题的最少代码和输入 |
课后练习¶
1 辨认输入输出¶
调用 simple_return(20, 21) 时,两个参数和返回值分别是什么?
提示
把数字按函数定义顺序放入旧价格和新价格。答案
旧价格参数为 20,新价格参数为 21,返回值为 `21 / 20 - 1 = 0.05`。2 判断返回值¶
函数内部只写 print(result),没有 return,外部变量通常保存什么?
提示
屏幕显示和函数交回结果是两项动作。答案
外部变量通常保存 `None`。函数虽然显示了内容,却没有通过 `return` 交回数值。3 阅读报错¶
三项列表读取 prices[3],最可能出现哪类错误,为什么?
提示
三项列表的有效位置是 0、1、2。答案
会出现 `IndexError`,因为索引 3 已经超出列表范围。先打印长度与索引可以确认。4 动手修改¶
写一个 price_change 函数,接收旧价格和新价格,检查两者都不小于零,再返回差值。
提示
先用 `if` 和 `raise ValueError` 检查,最后写 `return new_price - old_price`。答案
一种写法是 `def price_change(old_price, new_price):`,函数内部检查 `old_price < 0 or new_price < 0` 时抛出错误,随后返回 `new_price - old_price`。注意缩进。5 开放思考¶
一个函数同时下载数据、计算指标、生成信号、修改持仓和画图,会给研究检查带来什么困难?
评价要点
回答应讨论职责混合、输入输出不清、难以最小复现、修改一个环节影响其他环节,以及无法分别测试数据、策略和展示。下一课会认识量化研究最常用的三个第三方包,并把列表套字典转换成带行列名称的表格。继续阅读第 7 课 NumPy、pandas 和 Matplotlib。