阶段 11 讲义
数学 11h:高数 · 不定积分(换元与分部)
编程 9h:模块与推导式
第一部分 · 数学
阶段 3–7 我们一直在做"求导"。这一阶段开始做它的逆运算,也就是积分。
求导:知道位置,求速度。积分:知道速度,求走了多远。
积分比求导难得多,原因很硬:求导有固定套路(背公式+链式法则),积分没有。同样的函数,换个形式就得换方法。这一阶段学的换元和分部,就是两把最常用的钥匙。
1 原函数与不定积分
定义:如果 $F'(x) = f(x)$,就说 $F(x)$ 是 $f(x)$ 的一个原函数。
不定积分就是"求所有原函数":
$$\int f(x)\,dx = F(x) + C$$
1.1 那个 $+C$ 是什么(别小看它)
因为常数的导数是 0,所以 $F(x)$、$F(x)+1$、$F(x)-100$ 导数完全一样。
"求原函数"的答案永远是一族,彼此只差一个常数。
$$\int 2x\,dx = x^2 + C \quad (C \text{ 是任意常数})$$
验证:$(x^2+C)' = 2x$ ✓(对任何 $C$ 都成立)
⚠️ 忘记 $+C$ 是考试里最常见的扣分点。
更重要的原因:$C$ 在定积分里会自动消掉,但在解微分方程时$C$ 就是初值,
物理意义完全不同。养成写 $C$ 的习惯。
1.2 积分的两大直觉
| 直觉 | 说明 |
|---|---|
| 求导的逆运算 | 积分完求导,应该回到原函数(这是验算方法) |
| 曲线下的面积 | 阶段 12 讲定积分时展开 |
💡 验算方法:算完积分,立刻求导看看能不能回到原式。
这是唯一能自己检查积分对不对的办法,一定要用。
2 基本积分表(必背)
这些是"背下来"的部分,就像乘法口诀。
| 积分 | 结果 | 注意 |
|---|---|---|
| $\int x^n\,dx$ | $\dfrac{x^{n+1}}{n+1} + C$ | ⚠️ $n \ne -1$ |
| $\int \dfrac{1}{x}\,dx$ | $\ln\lvert x\rvert + C$ | 这就是 $n=-1$ 的情况 |
| $\int e^x\,dx$ | $e^x + C$ | 唯一"求积不变"的 |
| $\int a^x\,dx$ | $\dfrac{a^x}{\ln a} + C$ | |
| $\int \sin x\,dx$ | $-\cos x + C$ | ⚠️ 有负号 |
| $\int \cos x\,dx$ | $\sin x + C$ | 没负号 |
| $\int \dfrac{1}{1+x^2}\,dx$ | $\arctan x + C$ | |
| $\int \dfrac{1}{\sqrt{1-x^2}}\,dx$ | $\arcsin x + C$ | |
| $\int \sec^2 x\,dx$ | $\tan x + C$ |
⚠️ 最容易错的三个:
① $\int \sin x\,dx = -\cos x + C$(有负号)
② $\int \frac{1}{x}dx = \ln|x| + C$,不是 $\frac{x^0}{0}$(那是除以 0)
③ $\int x^n dx$ 的公式要求 $n \ne -1$,$n=-1$ 时单独记
3 换元积分法(一):凑微分 ⭐
核心思想:把积分"凑"成基本积分表里的样子。
原理(就是链式法则反过来):
$$\int f(g(x)) \cdot g'(x)\,dx = \int f(u)\,du \quad (\text{令 } u = g(x))$$
3.1 完整例子
求 $\int 2x\cos(x^2)\,dx$
观察:$2x$ 恰好是 $x^2$ 的导数。这不是巧合,是信号。
第一步:令 $u = x^2$
$$du = 2x\,dx$$
第二步:代入(整个积分里 $2x\,dx$ 被 $du$ 整体替换)
$$\int 2x\cos(x^2)\,dx = \int \cos u\,du$$
第三步:套基本积分表
$$= \sin u + C$$
第四步:换回 $x$(⚠️ 务必回代)
$$= \sin(x^2) + C$$
验算:$\dfrac{d}{dx}\sin(x^2) = \cos(x^2)\cdot2x = 2x\cos(x^2)$ ✓(sympy 实算确认)
3.2 怎么看出该凑什么
问自己:"这个积分里,有没有一部分是另一部分的导数?"
| 看到 | 想到 |
|---|---|
| $\int x e^{x^2}dx$ | $x$ 是 $x^2$ 的导数的一半 → 令 $u = x^2$ |
| $\int \frac{2x}{1+x^2}dx$ | $2x$ 是 $1+x^2$ 的导数 → 令 $u = 1+x^2$ |
| $\int \frac{1}{x\ln x}dx$ | $\frac1x$ 是 $\ln x$ 的导数 → 令 $u = \ln x$ |
| $\int \sin x\cos x\,dx$ | 任选一个当 $u$ 都行 |
最后一个例子两种做法(结果形式不同但等价):
$$\int \sin x\cos x\,dx = \frac{\sin^2 x}{2} + C \quad \text{或} \quad -\frac{\cos^2 x}{2} + C$$
看起来是两个答案!其实只差一个常数($\frac{\sin^2x + \cos^2x}{2} = \frac12$),在不定积分里算同一个答案。
💡 所以不定积分的答案不唯一,求导验算是唯一的判据。
3.3 完整例子二
求 $\int \dfrac{1}{x\ln x}\,dx$
令 $u = \ln x$,则 $du = \dfrac{1}{x}dx$。
$$\int \frac{1}{x\ln x}dx = \int \frac{1}{\ln x}\cdot\frac{1}{x}dx = \int \frac{1}{u}du = \ln|u| + C = \ln|\ln x| + C$$
验算:$\dfrac{d}{dx}\ln(\ln x) = \dfrac{1}{\ln x}\cdot\dfrac1x = \dfrac{1}{x\ln x}$ ✓(sympy 实算确认)
4 换元积分法(二):第二类换元(了解)
凑微分是"把 $x$ 换掉";第二类换元是"把 $x$ 换成别的",主要用于去掉根号。
| 遇到 | 令 |
|---|---|
| $\sqrt{a^2 - x^2}$ | $x = a\sin t$ |
| $\sqrt{a^2 + x^2}$ | $x = a\tan t$ |
| $\sqrt{x^2 - a^2}$ | $x = a\sec t$ |
例子:$\int \dfrac{1}{\sqrt{1-x^2}}dx$ 直接查表就是 $\arcsin x + C$,
它就是"三角换元"的结果,所以背下来就行(sympy 实算确认)。
💡 大一上你主要掌握"凑微分"。第二类换元知道思路即可,
真遇到复杂的直接交给计算机(阶段 11 之后你会用 sympy)。
5 分部积分法 ⭐⭐(重点)
什么时候用:被积函数是两种不同类型函数的乘积,比如 $x e^x$、$x\cos x$、$\ln x$。
公式(由乘积求导法则 $(uv)' = u'v + uv'$ 变形而来):
$$\int u\,dv = uv - \int v\,du$$
记忆口诀:"前后交换,符号变负",$u\,dv$ 换成 $v\,du$,中间加个减号。
5.1 完整例子一
求 $\int x e^x\,dx$
选 $u$ 和 $dv$($u$ 是要"求导"的那个,$dv$ 是要"积分"的那个):
$$u = x \quad\Rightarrow\quad du = dx, \qquad dv = e^x dx \quad\Rightarrow\quad v = e^x$$
套公式:
$$\int x e^x dx = x\cdot e^x - \int e^x dx = xe^x - e^x + C = (x-1)e^x + C$$
验算:$\dfrac{d}{dx}[(x-1)e^x] = e^x + (x-1)e^x = xe^x$ ✓(sympy 实算确认)
5.2 完整例子二:$\int \ln x\,dx$(巧妙的"补 1")
看不出是乘积?补一个 $1$:
$$u = \ln x \quad\Rightarrow\quad du = \frac1x dx, \qquad dv = dx \quad\Rightarrow\quad v = x$$
$$\int \ln x\,dx = x\ln x - \int x\cdot\frac1x dx = x\ln x - \int 1\,dx = x\ln x - x + C$$
验算:$\dfrac{d}{dx}[x\ln x - x] = \ln x + 1 - 1 = \ln x$ ✓
⭐ "补 1"是分部积分的常用技巧。看到 $\ln x$、$\arctan x$、$\arcsin x$
单独出现时,都补一个 $dv = dx$。
5.3 完整例子三:分部两次
求 $\int x^2 e^x\,dx$
第一次($u = x^2$):$= x^2e^x - \int 2xe^x dx$
第二次(对 $\int xe^x dx$ 再用一次,上面算过是 $(x-1)e^x$):
$$= x^2e^x - 2(x-1)e^x + C = (x^2 - 2x + 2)e^x + C$$
验算:sympy 实算确认 ✓
💡 规律:$x^n e^x$ 要分部 $n$ 次,每次都让 $x$ 的次数降一点。
这就是"分部的作用",降低复杂度。
5.4 完整例子四:分部两次"绕回来"(进阶)
求 $\int e^x\sin x\,dx$
第一次($u=\sin x$,$dv=e^xdx$):$= e^x\sin x - \int e^x\cos x\,dx$
第二次($u = \cos x$):$= e^x\sin x - \left[e^x\cos x + \int e^x\sin x\,dx\right]$
注意:$\int e^x\sin x\,dx$ 又出现了!设它等于 $I$:
$$I = e^x\sin x - e^x\cos x - I \quad\Longrightarrow\quad 2I = e^x(\sin x - \cos x)$$
$$\boxed{I = \frac{e^x(\sin x - \cos x)}{2} + C}$$
验算:sympy 实算确认 ✓(结果形式为 $-\frac{\sqrt2}{2}e^x\cos(x+\frac\pi4)$,与上式等价)
⭐ "绕回来"是分部积分的经典套路:
做到一半发现原积分又出现了,就把整个式子当成方程解。
这种"把未知当已知来解"的思路,在数学里反复出现(阶段 16 的特征值也是)。
6 怎么选 $u$(LIATE 顺序)
分部积分成功的关键是选对 $u$。经验顺序(排在前面的优先当 $u$):
| 顺序 | 类型 | 例子 |
|---|---|---|
| L | 对数 Log | $\ln x$ |
| I | 反三角 Inverse trig | $\arctan x$ |
| A | 代数 Algebraic | $x^n$ |
| T | 三角 Trig | $\sin x$ |
| E | 指数 Exponential | $e^x$ |
口诀:"反对幂指三"(反三角、对数、幂函数、指数、三角,中文教材的常见版本)。
为什么这样选:$u$ 要被求导,我们希望求导后变简单。
$\ln x$ 求导变 $\frac1x$(简单多了);$e^x$ 求导不变(最不该当 $u$)。
验证上面例子:$\int x e^x dx$,$x$ 是代数(A),$e^x$ 是指数(E),A 在 E 前 → $u = x$ ✓
7 常见的坑
| 坑 | 说明 |
|---|---|
| 忘记 $+C$ | 不定积分必须有,考试必扣分 |
| $\int \sin x\,dx$ 漏负号 | 是 $-\cos x + C$ |
| $\int \frac1x dx$ 写成 $\frac{x^0}{0}$ | 除以 0!应该是 $\ln\lvert x\rvert + C$ |
| 换元后忘记回代 | 令 $u$ 就得换回 $x$,答案里不能留 $u$ |
| 换元后忘了改 $dx$ | $du = g'(x)dx$ 必须改,漏了就算错 |
| 分部积分选错 $u$ | 选错会越算越复杂。按 LIATE / "反对幂指三" |
| 以为不定积分答案唯一 | 差一个常数都是对的答案。用求导验算判对错 |
| 算完不验算 | 积分没有"代入检查"的办法,求导验算是唯一的自检手段 |
8 🧠 这跟认知科学有什么关系
这一节的对应关系是我验证过的(用代码实算),但心理学结论请对照文献。
反应时分布 = 一个概率密度函数,而它的均值就是一个积分。
8.1 认知科学里的"反应时"为什么不服从正态分布
经典事实:反应时的分布右偏(有一小撮特别慢的试次),不是正态分布。
常用模型之一:对数正态分布(log-normal)。它的密度函数是
$$f(t) = \frac{1}{t\sigma\sqrt{2\pi}}\exp\left(-\frac{(\ln t - \mu)^2}{2\sigma^2}\right), \qquad t > 0$$
它的期望(平均反应时)是一个积分:
$$\mathbb{E}[RT] = \int_0^\infty t\,f(t)\,dt$$
8.2 实算给你看
我取 $\mu = 0.2$、$\sigma = 0.4$,做了三件事:
| 方法 | 结果 |
|---|---|
| 理论公式 $\mathbb{E}[RT] = e^{\mu + \sigma^2/2}$ | $1.3231$ |
数值积分 $\int_0^\infty t f(t)dt$(scipy.integrate.quad) |
$1.32313$ |
| 蒙特卡洛模拟(10000 个样本求平均) | $1.3255$ |
三者吻合,解析公式、数值积分、模拟实验,这是验证一个理论结果的三条路。
再看一个"累积概率"(就是积分):
$$P(RT < 1.0) = \int_0^{1.0} f(t)\,dt$$
| 方法 | 结果 |
|---|---|
| 理论 CDF | $0.3085$ |
| 数值积分 | $0.308538$ |
| 模拟(10000 次里有多少小于 1.0) | $0.3087$ |
8.3 为什么这件事重要
认知科学里的模型,一多半都要算积分:
| 模型 / 量 | 积分的角色 |
|---|---|
| 对数正态 RT 模型 | 均值、分位数都是积分 |
| ex-Gaussian(正态 + 指数卷积) | 密度函数是两个分布的卷积积分 |
| 漂移扩散模型(DDM) | RT 分布是"首达时间"的密度,靠解偏微分方程/积分得到 |
| 信号检测论 | $d'$ 的定义里含正态分布积分 |
| 心理物理函数 | 常是累积正态积分 $\int_{-\infty}^{x}$ |
| 贝叶斯推断 | 后验分布的分母是个积分(归一化常数) |
"积分"在认知科学里是"从模型算出可观测量的必经之路",不是数学课的作业。
你只会在考试里手算换元和分部;真做研究时,这些积分全部交给计算机,
但你必须知道它们是什么、为什么在那里,否则看不懂论文的公式。⚠️ 诚实说明:
- 上面的积分值、模拟结果都是我实算的(可复现),
"对数正态的期望是 $e^{\mu+\sigma^2/2}$"是数学事实。
- 但"反应时到底服从什么分布""该用哪个模型"在领域内仍有争论
(对数正态、ex-Gaussian、Gamma、偏正态都被用过),
请以 Psychological Methods、Journal of Mathematical Psychology 等文献为准。
我保证数学,不保证模型选择。
9 数学作业 S11
- [ ] 1. 求 $\int 3x^2\,dx$,并用求导验算
- [ ] 2. 求下列不定积分(背过的基本表):
(a) $\int \frac{1}{x}dx$ (b) $\int \sin x\,dx$ (c) $\int e^x dx$ (d) $\int \frac{1}{1+x^2}dx$
- [ ] 3. 用凑微分求 $\int 2x\cos(x^2)\,dx$
- [ ] 4. 用凑微分求 $\int \frac{2x}{1+x^2}\,dx$
- [ ] 5. 用凑微分求 $\int xe^{x^2}dx$
- [ ] 6. 用分部积分求 $\int xe^x dx$
- [ ] 7.(重点)用分部积分求 $\int x\cos x\,dx$
- [ ] 8.(重点)用分部积分求 $\int \ln x\,dx$(记得"补 1")
- [ ] 9. 用分部积分两次求 $\int x^2e^x dx$
- [ ] 10. 下面哪个是 $\int 2x\,dx$ 的正确答案?(可能不止一个)
(a) $x^2$ (b) $x^2 + 5$ (c) $x^2 - 3$ (d) $x^2 + C$
- [ ] 11.(Python)用 sympy 验证作业 3、6、7、8、9,并写一个自动验算函数
第 11 题参考代码:
import sympy as sp
from sympy import symbols, sin, cos, exp, log, sqrt, simplify, integrate, diff
x = symbols('x', positive=True)
def check(expr, name=""):
"""求积分,再用求导验算。返回结果,验算失败会报警。"""
F = simplify(integrate(expr, x))
back = simplify(diff(F, x) - expr)
ok = (back == 0)
print(f"{name or expr}")
print(f" 积分结果 = {F} + C")
print(f" 求导验算 : {'✓ 通过' if ok else f'✗ 失败,差 {back}'}")
return F
check(2 * x * cos(x**2), "∫2x·cos(x²)dx (凑微分)")
check(x * exp(x), "∫x·eˣ dx (分部)")
check(x * cos(x), "∫x·cos x dx (分部)")
check(log(x), "∫ln x dx (补1)")
check(x**2 * exp(x), "∫x²eˣ dx (分部两次)")
check(sin(x)**2, "∫sin²x dx (进阶)")
print()
print("=== 验证 '不定积分答案不唯一,只差常数' ===")
F1 = integrate(sin(x) * cos(x), x) # sympy 给的形式
F2 = sin(x)**2 / 2 # 教材常见形式
print("sympy 给的 :", F1)
print("教材常见写法:", F2)
print("两者之差 :", simplify(F1 - F2), " <- 是常数就对")
💡 这个
check()函数直接存进你的工具箱。
以后任何积分,不管是作业还是论文里的,都可以用它确认对错。
这就是"用代码验证数学"的具体样子。
第二部分 · 编程
10 为什么要拆成多个文件
到目前为止,你的程序都在一个文件里。但真实项目会这样:
experiment/
├── main.py 主流程
├── io_utils.py 读写数据的函数
├── analysis.py 统计分析的函数
├── plot_utils.py 画图的函数
└── config.py 参数设置
为什么要拆:
| 理由 | 说明 |
|---|---|
| 好找 | 改画图逻辑时,直接开 plot_utils.py,不用在 800 行里翻 |
| 好复用 | 下个项目直接用 io_utils.py |
| 好协作 | 别人改画图,你改统计,不冲突 |
| 能测试 | 每个文件可以单独测 |
一个文件只干一件事。文件超过 200–300 行就该考虑拆了。
11 import 的几种写法
import math # ① 最简单的:用的时候 math.sqrt(4)
import numpy as np # ② 起别名(numpy 的标准写法)
import pandas as pd # pandas 的标准写法
from pathlib import Path # ③ 只导入一个东西:直接用 Path(...)
from math import sqrt, pi # ④ 导入多个:直接用 sqrt(4)
四种写法的对比:
| 写法 | 怎么用 | 什么时候用 |
|---|---|---|
import math |
math.sqrt(4) |
通用,最清楚 |
import numpy as np |
np.sqrt(4) |
库名太长,社区有约定俗成的缩写 |
from pathlib import Path |
Path("a.txt") |
只用一两个东西 |
from math import * |
sqrt(4) |
⚠️ 不要用(见下) |
⚠️ 永远不要写
from math import *:
它把库里所有名字都倒进你的命名空间,可能覆盖你的变量。
比如你自己写了个sqrt函数,被库里的悄悄替换掉,这种 bug 极难查。
别名不是随便起的:np、pd、plt 是社区约定,别人一看就懂。别自己发明。
12 写自己的模块
"模块"就是一个 .py 文件。任何 .py 文件都能被 import。
建一个 utils.py(和主程序放在同一个目录):
"""我的工具箱:常用的几个函数。"""
def mean_of(nums):
"""求平均值。空列表返回 0。"""
if not nums:
return 0
return sum(nums) / len(nums)
def describe(nums):
"""返回 (最小值, 最大值, 平均值) 三元组。"""
return min(nums), max(nums), mean_of(nums)
PI_ISH = 3.14159 # 模块级变量,也能被导入
在主程序 main.py 里用它:
import utils # 导入整个模块
scores = [88, 92, 75, 96, 81]
print(utils.mean_of(scores)) # 86.4
print(utils.describe(scores)) # (75, 96, 86.4)
print(utils.PI_ISH) # 3.14159
from utils import mean_of # 只导入一个函数
print(mean_of(scores)) # 86.4
import utils as u # 起别名
print(u.mean_of(scores)) # 86.4
⚠️ 模块名就是文件名(去掉
.py),而且必须和主程序在同一个目录,
或者在一个 Python 能找到的目录里(阶段 16 详细讲)。💡 import 时会执行整个文件!这是理解
__main__的关键(下一节)。
13 if __name__ == "__main__":(重要的约定)
问题:如果 utils.py 里写了测试代码,别人 import utils 时这些测试也会跑一遍。
# utils.py 里直接写测试的后果
print("正在初始化 utils") # import 时也会打印!
解决办法,把"只有直接运行才执行"的代码放进这个块:
"""我的工具箱。"""
def mean_of(nums):
"""求平均值。空列表返回 0。"""
if not nums:
return 0
return sum(nums) / len(nums)
if __name__ == "__main__":
# 只有"直接运行 python utils.py"时才执行
# 被别人 import 时,这里不会跑
print("自测开始")
assert mean_of([1, 2, 3]) == 2
assert mean_of([]) == 0
print("自测通过")
原理:
| 情况 | __name__ 的值 |
if 成立吗 |
|---|---|---|
直接运行 python utils.py |
"__main__" |
✓ 执行 |
被导入 import utils |
"utils" |
✗ 不执行 |
⭐ 这是 Python 最优雅的约定之一:
同一个文件,既能当"程序"直接跑,又能当"工具箱"被导入,互不干扰。
你以后写的每一个.py文件都应该有这个块。
14 推导式(让循环变一行)
推导式(comprehension)把"建空列表 + 循环 + append"压成一行。
# 传统写法(4 行)
squares = []
for i in range(5):
squares.append(i ** 2)
# 推导式(1 行,完全等价)
squares = [i ** 2 for i in range(5)]
print(squares) # [0, 1, 4, 9, 16]
怎么读:从右往左读,"对 range(5) 里的每个 i,算出 i**2,收进列表"。
四种推导式:
# ① 列表推导式
squares = [i ** 2 for i in range(5)] # [0, 1, 4, 9, 16]
# ② 带条件(只收偶数)
evens = [i for i in range(10) if i % 2 == 0] # [0, 2, 4, 6, 8]
# ③ 字典推导式
sq_dict = {i: i ** 2 for i in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
# ④ 集合推导式
unique_len = {len(w) for w in ["ab", "cd", "efg"]} # {2, 3}
带 if-else 的写法(注意 if 的位置变了):
# 条件放后面 = "过滤"(只要满足条件的)
evens = [i for i in range(10) if i % 2 == 0] # [0, 2, 4, 6, 8]
# if-else 放前面 = "变换"(每个都要,但要分两种情况)
labels = ["偶" if i % 2 == 0 else "奇" for i in range(4)]
print(labels) # ['偶', '奇', '偶', '奇']
⚠️ 这两个
if的位置完全不同,作用也不同:
for后面的if是"筛子"(决定要不要);
for前面的if-else是"加工"(每个都要,产出不同)。
这是最容易混淆的地方。
实战:处理实验数据
raw = [" 0.523 ", " 1.234", "", "0.789 "]
# 传统写法
rts = []
for item in raw:
item = item.strip()
if item:
rts.append(float(item))
print(rts) # [0.523, 1.234, 0.789]
# 推导式写法
rts = [float(s.strip()) for s in raw if s.strip()]
print(rts) # [0.523, 1.234, 0.789]
⚠️ 推导式什么时候不该用:
逻辑超过一层循环 + 一个条件时,写回for循环更清楚。
有人能把三层嵌套推导式写一行,那不是本事,是阅读障碍。
可读性永远优先。
15 生成器表达式(省内存的版本)
把 [] 换成 (),就得到生成器:
gen = (i ** 2 for i in range(5))
print(gen) # <generator object ...> 不是列表!
print(list(gen)) # [0, 1, 4, 9, 16] 转成列表才能看
区别:
列表推导式 [] |
生成器 () |
|
|---|---|---|
| 立刻算吗 | 立刻全部算好 | 用到才算(惰性) |
| 占内存 | 全部结果 | 只有一个 |
| 能重复用吗 | 能 | 不能(用完就空) |
| 什么时候用 | 要反复访问、要 len() |
数据量大、只遍历一次 |
# 1000 万个数的平方:列表会吃掉几百 MB 内存
# 生成器几乎不占内存,但要重来必须重建
big_gen = (i ** 2 for i in range(10_000_000))
total = sum(big_gen) # 这样用才对:边算边加,不占内存
💡 实战建议:读大文件、算大和时用生成器;
小数据(几千个以内)用列表推导式就行,别想太多。
16 🔧 技能卡 11:怎么把代码拆成多个文件
技能卡 10 教你处理文件;这张卡教你处理"代码文件",怎么把一个 800 行的脚本变成好维护的项目。
16.1 决策一:该拆吗?
| 症状 | 该拆 |
|---|---|
| 单文件超过 200–300 行 | ✓ |
| 同一段代码复制了 3 次 | ✓(提取成函数,放进工具模块) |
| 想单独测某部分 | ✓ |
| 只有 50 行、一件事 | ✗ 别拆,会更麻烦 |
⚠️ 别过度拆分:3 个文件能解决的事拆成 10 个,
你会在 import 关系里迷路。先从"一个文件"开始,痛了再拆。
16.2 决策二:怎么命名和分层
| 文件类型 | 命名 | 放什么 |
|---|---|---|
| 入口 | main.py / run.py |
只有流程编排,不写具体逻辑 |
| 工具 | utils.py / io.py |
通用的、和具体项目无关的函数 |
| 领域 | analysis.py / plot.py |
这个项目专用的逻辑 |
| 配置 | config.py |
参数、路径、常量 |
| 测试 | test_xxx.py |
检查用例 |
依赖方向(很重要):
main.py ──> analysis.py ──> utils.py
│ ▲
└──────────────────────────────┘
允许:main 导入 analysis 和 utils
禁止:utils 导入 main ← 会出现循环导入,直接报错
⚠️ 底层模块不能反过来导入上层模块。
utils.py里写import main会让 Python 陷入循环导入,报 ImportError。
16.3 决策三:写新文件时的固定模板
每个 .py 文件都该长这样:
"""一句话说明这个文件是干什么的。—— 这是模块文档字符串(docstring)"""
from pathlib import Path # 标准库
import numpy as np # 第三方库
# ---- 常量 ----
DEFAULT_ENCODING = "utf-8"
# ---- 函数 ----
def do_something(x):
"""一句话说明这个函数干什么。"""
...
if __name__ == "__main__":
# 自测代码:只有直接运行时才跑
...
16.4 三条新手铁律
① 每个文件开头写一句 docstring
不用长,一句话:"这个文件负责读写实验数据"。三个月后的你会感谢现在的你。
② import 全部写在文件顶部
不要在函数中间 import(除非有特殊理由)。要什么依赖,一眼就能看完。
③ 每个可运行的文件都加 if __name__ == "__main__":
这样它既能当脚本跑,又能被别人导入。这是 Python 的基本礼节。
💡 遇到
ImportError: No module named 'xxx'怎么办:
按顺序查三件事:
1. 拼写对不对(文件名和 import 名完全一致,注意大小写)
2. 在不在同一个目录(或者在同一目录的子目录里)
3. 有没有循环导入(A 导 B、B 又导 A)90% 的 ImportError 是这三条之一。
17 🧠 这跟认知科学有什么关系(编程侧)
概念性内容,无法用代码验证,请对照文献。
模块化 ↔ 认知科学里的"认知模块性"(modularity of mind),一个跨学科的巧合。
17.1 先说编程侧的事实
模块化编程的核心主张:
| 主张 | 理由 |
|---|---|
| 功能分离 | 每个模块只管一件事 |
| 接口明确 | 模块之间只通过定义好的函数通信 |
| 可替换 | 只要接口不变,内部实现随便换 |
| 独立演化 | 改一个模块不影响其他 |
好处:好维护、好测试、好复用,这也是它成为软件工程主流的原因。
17.2 认知科学里的"模块性"(要非常小心地区分)
Fodor (1983) 在 The Modularity of Mind 里提出:心理的某些部分可能是模块:
| Fodor 的模块特征 | 编程模块的对应 |
|---|---|
| 领域特异性(只处理特定输入) | 一个模块只干一件事 |
| 信息封装(内部不可被其他系统访问) | 内部变量不暴露,只暴露函数 |
| 强制性(自动触发,不受控制) | 被调用就执行 |
| 快速(不依赖全局资源) | 轻量、无全局依赖 |
| 固定的神经架构 | 确定的实现 |
看起来很像,但要非常小心。
⚠️ 关键区分(这一段最重要):
- Fodor 的模块性是一个关于心理如何组织的经验假说,它有明确的、很苛刻的判据,
而且 Fodor 本人认为只有低层知觉(如视觉、语言输入系统)才是模块,
高层认知(如推理、信念)不是。
- 后来 "大规模模块性"(massive modularity)(Cosmides & Tooby、Sperber 等)
把这个概念大大扩展,认为几乎一切都模块化,这和 Fodor 的原意相当不同。
- "认知模块性"这个主张至今仍有争议,神经科学证据也有支持和反对。"编程要模块化"是一条工程经验;"心灵是模块化的"是一个有争议的科学假说。
两者字面相似纯属巧合,不要用前者去论证后者。
那这节的意义是什么:
它的意义是"类比本身",当你在认知科学课上听到"模块性"时,
你脑子里会有一个非常具体的、可操作的表征("就像把代码拆成 import 的模块")。
这种"用熟悉的系统去理解陌生的系统",正是类比推理在起作用,
而类比推理本身就是认知科学的研究对象。
⚠️ 诚实说明:
- Fodor 模块性的四条核心特征(领域特异性、信息封装、强制性、快速)是文献中的标准概括,
请以 The Modularity of Mind 原文及后续讨论(如 The Mind Doesn't Work That Way)为准。
- "编程模块化"和"认知模块性"是不同的东西,本节只是指出表面相似,
绝不是说"所以心灵像软件"。请务必对照文献。
18 编程作业 S11
- [ ] 1. 创建一个
mytools.py,里面写mean_of(nums)和describe(nums),在主程序里 import 使用 - [ ] 2. 给
mytools.py加上if __name__ == "__main__":自测块,解释加与不加的区别(动手验证) - [ ] 3. 用列表推导式生成 1–20 中所有 3 的倍数
- [ ] 4. 用字典推导式建
{1: 1, 2: 4, 3: 9, ..., 10: 100} - [ ] 5. 把下面的循环改写成推导式:
result = [] for w in ["Hi", "there", "cognition"]: if len(w) > 3: result.append(w.upper())- [ ] 6. 用推导式从
[" 0.5 ", "bad", "1.2", ""]里提取所有能转成浮点数的值提示:需要 try/except 的话,推导式就不合适了,先想想能不能不用
- [ ] 7.(重点)分别用列表推导式和生成器表达式求 1 到 1000000 的和,比较内存占用
用
sys.getsizeof()看看差别 -
[ ] 8. 解释下面两个推导式的区别(都合法,但意义不同):
a = [i for i in range(10) if i % 2 == 0]
b = [i if i % 2 == 0 else -1 for i in range(10)]
- [ ] 9.(技能)把阶段 10 作业里的read_records拆成io_utils.py,写一个main.py调用它
- [ ] 10.(Python)建stats_utils.py,实现mean_of、median_of、stdev_of,附自测块和检查用例
- [ ] 6. 用推导式从
第 7 题参考写法:
import sys
import tracemalloc
n = 1_000_000
# 列表推导式:立刻把 100 万个数全部算出来存好
tracemalloc.start()
lst = [i for i in range(n)]
_, peak_list = tracemalloc.get_traced_memory()
tracemalloc.stop()
print("列表推导式峰值内存:", round(peak_list / 1024 / 1024, 1), "MB") # 38.6 MB
# 生成器:边算边丢,内存几乎不涨
tracemalloc.start()
total = sum(i for i in range(n))
_, peak_gen = tracemalloc.get_traced_memory()
tracemalloc.stop()
print("生成器求和峰值内存:", round(peak_gen / 1024 / 1024, 4), "MB") # 0.0004 MB
print("sum(生成器) =", total) # 499999500000
# 顺便看看"骨架"大小
print("sys.getsizeof(列表) =", sys.getsizeof(lst), "字节") # 8448728
print("sys.getsizeof(生成器) =", sys.getsizeof(i for i in range(n)), "字节") # 200
实跑结果(我在本机实测):
| 做法 | 峰值内存 |
|---|---|
| 列表推导式 | 38.6 MB |
| 生成器求和 | 0.0004 MB |
差了约 10 万倍。
💡 注意
sys.getsizeof(lst)只算列表本身的骨架(8.06 MB),
不含里面 100 万个 int 对象(每个约 28 字节)。
想测真实总量要用tracemalloc,上表用的是tracemalloc,才是可信的数字。
第 8 题答案:
a = [i for i in range(10) if i % 2 == 0]
# for 后面的 if 是"筛子":只留下偶数
# → [0, 2, 4, 6, 8]
b = [i if i % 2 == 0 else -1 for i in range(10)]
# for 前面的 if-else 是"加工":每个都要,偶数留原值,奇数变 -1
# → [0, -1, 2, -1, 4, -1, 6, -1, 8, -1]
一个
[0,2,4,6,8],一个[0,-1,2,-1,4,-1,6,-1,8,-1],长度就不一样。
记法:if在后面 → 筛掉;if-else在前面 → 变换。
第 10 题参考写法:
"""统计工具:均值、中位数、标准差。全用纯 Python 实现。"""
def mean_of(nums):
"""算平均值。空列表返回 0。"""
if not nums:
return 0
return sum(nums) / len(nums)
def median_of(nums):
"""算中位数。先排序,再看奇偶。"""
if not nums:
return 0
s = sorted(nums)
n = len(s)
mid = n // 2
if n % 2 == 1:
return s[mid] # 奇数个:正中间
return (s[mid - 1] + s[mid]) / 2 # 偶数个:中间两个的平均
def stdev_of(nums):
"""算样本标准差(除以 n-1)。少于 2 个数时返回 0。"""
if len(nums) < 2:
return 0
m = mean_of(nums)
variance = sum((x - m) ** 2 for x in nums) / (len(nums) - 1)
return variance ** 0.5
if __name__ == "__main__":
data = [2, 4, 4, 4, 5, 5, 7, 9]
assert mean_of(data) == 5.0, mean_of(data)
assert median_of(data) == 4.5, median_of(data)
assert abs(stdev_of(data) - 2.13809) < 1e-4, stdev_of(data)
assert mean_of([]) == 0
assert median_of([7]) == 7
print("全部检查用例通过")
💡 注意
stdev_of里用了生成器表达式sum((x-m)**2 for x in nums),
不需要中间列表,边算边加。这正是生成器最自然的用法。
本阶段自评
- 数学:为什么不定积分的答案要 $+C$?$C$ 有什么实际意义?
- 数学:$\int \frac1x dx$ 为什么不能套 $\frac{x^{n+1}}{n+1}$?
- 数学:凑微分的核心思路是什么?"该凑什么"怎么看出来?
- 数学:分部积分的公式是什么?由哪个求导法则推导来的?
- 数学:$\int \ln x\,dx$ 里的"补 1"是什么意思?
- 数学:分部积分里 $u$ 该按什么顺序选?为什么 $\ln x$ 优先当 $u$?
- 数学:怎么检查积分算得对不对?为什么这是唯一的自检手段?
- 编程:
import math和from math import sqrt有什么区别? - 编程:
if __name__ == "__main__":解决了什么问题? - 编程:推导式里
if在for前面和后面,意义有什么不同? - 编程:生成器表达式和列表推导式,内存上差在哪?什么时候用哪个?
- 技能:遇到
ImportError,按顺序该查哪三件事? - 连接:Fodor 的"模块性"和编程的"模块化"是同一回事吗?为什么不能混用?
全部答得上 → 进阶段 12。
有任何一个答不上 → 回去补对应小节,别跳。