▤书架 ← 返回索引
▶目录

阶段 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

第 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

第 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),
不需要中间列表,边算边加。这正是生成器最自然的用法。


本阶段自评

  1. 数学:为什么不定积分的答案要 $+C$?$C$ 有什么实际意义?
  2. 数学:$\int \frac1x dx$ 为什么不能套 $\frac{x^{n+1}}{n+1}$?
  3. 数学:凑微分的核心思路是什么?"该凑什么"怎么看出来?
  4. 数学:分部积分的公式是什么?由哪个求导法则推导来的?
  5. 数学:$\int \ln x\,dx$ 里的"补 1"是什么意思?
  6. 数学:分部积分里 $u$ 该按什么顺序选?为什么 $\ln x$ 优先当 $u$?
  7. 数学:怎么检查积分算得对不对?为什么这是唯一的自检手段?
  8. 编程:import math 和 from math import sqrt 有什么区别?
  9. 编程:if __name__ == "__main__": 解决了什么问题?
  10. 编程:推导式里 if 在 for 前面和后面,意义有什么不同?
  11. 编程:生成器表达式和列表推导式,内存上差在哪?什么时候用哪个?
  12. 技能:遇到 ImportError,按顺序该查哪三件事?
  13. 连接:Fodor 的"模块性"和编程的"模块化"是同一回事吗?为什么不能混用?

全部答得上 → 进阶段 12。
有任何一个答不上 → 回去补对应小节,别跳。

最后更新:2026-09-26