阶段 12 讲义
数学 11h:高数 · 定积分及其应用
编程 9h:面向对象(一)
第一部分 · 数学
阶段 11 的不定积分是"求一族函数"。这一阶段的定积分是"求一个数"。
别小看这个区别:不定积分的答案里有 $C$,定积分的答案里没有,因为两个端点一减,$C$ 自己消掉了。
而"定积分是一个数"这件事,正是它在科学里如此有用的原因:它能给出可测量的量(面积、体积、总概率、平均值)。
1 从"面积"到定积分
设想:求曲线 $y = x^2$ 在 $[0,1]$ 上、与 $x$ 轴围成的面积。
方法:把它切成很多细长条,用小矩形近似,再把面积加起来。
- 每份宽度 $\Delta x = \frac{1}{n}$
- 取左端点的高 $f(x_i)$
- 总面积 $\approx \sum_{i=0}^{n-1} f(x_i)\Delta x$
$n$ 越大,近似越准。我实算给你看($f(x)=x^2$,区间 $[0,1]$):
| $n$ | 左黎曼和 | 右黎曼和 |
|---|---|---|
| 10 | $0.285000$ | $0.385000$ |
| 100 | $0.328350$ | $0.338350$ |
| 1000 | $0.332834$ | $0.333833$ |
| 100000 | $0.333328$ | $0.333338$ |
| 极限 | $\mathbf{1/3}$ | $\mathbf{1/3}$ |
两边从外往里夹,都趋向 $\frac13 \approx 0.3333333$。
定义:这个极限就是定积分
$$\int_a^b f(x)\,dx = \lim_{n\to\infty}\sum_{i=1}^{n} f(x_i)\,\Delta x$$
读法:$\int$ 是拉长的 S(Sum),$dx$ 是"无穷小的宽度"。
⚠️ 定积分是一个数,不定积分是一族函数。两者是完全不同的东西,
只是碰巧由同一个公式(牛顿-莱布尼茨)联系起来。
2 牛顿-莱布尼茨公式 ⭐⭐(本阶段的核心)
如果不用这个公式,每次算定积分都要去求黎曼和的极限,那会疯掉。
$$\boxed{\int_a^b f(x)\,dx = F(b) - F(a)}$$
其中 $F$ 是 $f$ 的任意一个原函数。
先求原函数,再代两个端点,上减下。
2.1 完整例子一
求 $\int_0^1 x^2\,dx$
第一步:找原函数 $F(x) = \dfrac{x^3}{3}$
第二步:代入两个端点,上减下
$$\int_0^1 x^2 dx = \left[\frac{x^3}{3}\right]_0^1 = \frac{1^3}{3} - \frac{0^3}{3} = \frac13$$
和黎曼和的极限一致 ✓
💡 方括号 $[F(x)]_a^b$ 是标准写法,表示"先求原函数,再代端点相减"。
写作业时用这个记号,既清楚又不容易漏端点。
2.2 完整例子二
求 $\int_0^{\pi} \sin x\,dx$
$$\int_0^{\pi}\sin x\,dx = [-\cos x]_0^{\pi} = (-\cos\pi) - (-\cos 0) = 1 - (-1) = 2$$
⚠️ 这里最容易错:$\cos\pi = -1$,前面还有个负号,所以 $-\cos\pi = 1$。
符号一乱就容易算成 0。建议老老实实一步一步写。
实算确认:$\int_0^\pi \sin x\,dx = 2$ ✓
2.3 完整例子三(含换元)
求 $\int_0^1 \dfrac{1}{1+x^2}dx$
原函数是 $\arctan x$,所以
$$[\arctan x]_0^1 = \arctan 1 - \arctan 0 = \frac{\pi}{4} - 0 = \frac{\pi}{4} \approx 0.7854$$
实算确认 ✓
⚠️ 定积分做换元时,别忘了同时换积分限!
如果令 $u = g(x)$,那么 $x=a$ 对应 $u=g(a)$,$x=b$ 对应 $u=g(b)$。
换了限就不用再换回 $x$,少一步,也少一次出错的机会。
3 定积分的性质
| 性质 | 式子 |
|---|---|
| 上下限交换变号 | $\int_a^b f = -\int_b^a f$ |
| 上下限相同为零 | $\int_a^a f = 0$ |
| 区间可加 | $\int_a^b f = \int_a^c f + \int_c^b f$ |
| 常数可提出 | $\int_a^b kf = k\int_a^b f$ |
| 可拆开 | $\int_a^b (f+g) = \int_a^b f + \int_a^b g$ |
| 奇函数在对称区间积分为 0 | $\int_{-a}^{a} f = 0$($f$ 为奇函数) |
| 偶函数在对称区间 | $\int_{-a}^{a} f = 2\int_0^a f$($f$ 为偶函数) |
奇偶性那条特别好用:
$$\int_{-1}^{1} x^3\,dx = 0 \quad (\text{奇函数,实算确认})$$
看都不用算,左半边的负面积和右半边的正面积正好抵消。
💡 考试实战:看到对称区间先想奇偶性。
能直接得 0 的题,别去老老实实算原函数。
4 微积分基本定理(变限积分求导)
这条定理把"求导"和"积分"彻底焊在了一起。
$$\frac{d}{dx}\int_a^x f(t)\,dt = f(x)$$
白话:"先积分再求导,回到原函数。"
为什么叫"基本定理":它证明了求导和积分互为逆运算。
这是整个微积分的地基。牛顿-莱布尼茨公式就是它的直接推论。
我实算给你看:
| $f(t)$ | $\int_0^x f(t)dt$ | $\frac{d}{dx}\left[\int_0^x f(t)dt\right]$ |
|---|---|---|
| $t^2$ | $\dfrac{x^3}{3}$ | $x^2$ ✓ |
| $\sin t$ | $1 - \cos x$ | $\sin x$ ✓ |
| $e^{-t^2}$ | $\dfrac{\sqrt{\pi}}{2}\mathrm{erf}(x)$ | $e^{-x^2}$ ✓ |
第三行有意思:$e^{-t^2}$ 没有初等原函数(这就是著名的"正态分布积分"),
但它的变限积分求导依然等于它自己,定理不需要你求出原函数。
⭐ 这就是为什么这条定理性感:
即使原函数写不出来,求导和积分的关系依然成立。
正态分布的累积分布函数 $\Phi(x) = \int_{-\infty}^x \frac{1}{\sqrt{2\pi}}e^{-t^2/2}dt$
就是靠这个才存在。
5 定积分的应用
5.1 应用一:平面图形的面积
两曲线之间的面积 = $\int_a^b (\text{上曲线} - \text{下曲线})\,dx$
例:求 $y = x$ 与 $y = x^2$ 在 $[0,1]$ 之间的面积。
先看谁在上面(在 $[0,1]$ 上 $x \ge x^2$),所以上减下:
$$S = \int_0^1 (x - x^2)dx = \left[\frac{x^2}{2} - \frac{x^3}{3}\right]_0^1 = \frac12 - \frac13 = \frac16$$
实算确认:$\frac16 \approx 0.16667$ ✓
⚠️ 一定要先判断"谁在上"。判反了答案会变成负数(虽然绝对值对)。
5.2 应用二:函数的平均值
$$\bar{f} = \frac{1}{b-a}\int_a^b f(x)\,dx$$
直觉:把所有点的高度加起来平均。积分就是"求和",除以长度就是"平均"。
例:$f(x) = x^2$ 在 $[0,3]$ 上的平均值
$$\bar{f} = \frac13\int_0^3 x^2 dx = \frac13\left[\frac{x^3}{3}\right]_0^3 = \frac13\cdot\frac{27}{3} = 3$$
实算确认 ✓
这直接对应物理量:平均速度 $= \frac{1}{T}\int_0^T v(t)dt$(总路程 ÷ 总时间)。
5.3 应用三:旋转体的体积
曲线绕 $x$ 轴旋转一周的体积:
$$V = \pi\int_a^b [f(x)]^2\,dx$$
为什么有 $\pi$ 和平方:切片是圆盘,半径 $f(x)$,面积 $\pi f^2$。
例:$y = x^2$ 绕 $x$ 轴转 $[0,1]$
$$V = \pi\int_0^1 x^4 dx = \pi\left[\frac{x^5}{5}\right]_0^1 = \frac{\pi}{5} \approx 0.6283$$
实算确认 ✓
6 反常积分(了解)
两种情况:区间无穷,或被积函数无界。
$$\int_1^\infty f(x)dx = \lim_{b\to\infty}\int_1^b f(x)dx$$
关键问题:收敛还是发散?
| 积分 | 结果 | 收敛? |
|---|---|---|
| $\int_1^\infty \dfrac{1}{x^2}dx$ | $1$ | ✓ 收敛 |
| $\int_1^\infty \dfrac{1}{x}\,dx$ | $+\infty$ | ✗ 发散 |
| $\int_0^\infty e^{-x}dx$ | $1$ | ✓ 收敛 |
| $\int_0^1 \dfrac{1}{\sqrt{x}}dx$ | $2$ | ✓ 收敛 |
(以上都是 sympy 实算结果)
⭐ 注意 $\frac{1}{x}$ 和 $\frac{1}{x^2}$ 的分界:
$\int_1^\infty \frac{1}{x^p}dx$ 在 $p > 1$ 时收敛,$p \le 1$ 时发散。
这个"$p=1$ 是分界线"的事实,是分析学里反复出现的模式。$\frac{1}{\sqrt x}$ 在 $0$ 附近虽然趋于无穷,但无穷得"不够快",所以积分收敛
"无穷大"也分快慢。
7 常见的坑
| 坑 | 说明 |
|---|---|
| 代端点时算错符号 | $[-\cos x]_0^\pi$:$-\cos\pi = +1$,一不留神就得 0 |
| 换元后忘了换积分限 | 换了 $u$ 就必须换上下限,否则结果必错 |
| 求面积时判错"谁在上" | 先比较或者画图,否则得负数 |
| 求面积时直接算积分不加绝对值 | 曲线穿过 $x$ 轴时要分段,否则正负相消 |
| 把定积分当成不定积分 | 定积分是一个数,答案里不应有 $+C$,也不应有 $x$ |
| 对称区间忘记用奇偶性 | 白白多算一堆,还容易错 |
| 误以为所有反常积分都收敛 | $\int_1^\infty\frac1x dx$ 发散,别想当然 |
| 混淆"面积"和"积分" | 积分可以有负值,面积永远是正的 |
⚠️ "面积"和"积分"的区别:
$\int_0^{2\pi}\sin x\,dx = 0$,但曲线与 $x$ 轴围成的面积是 4,不是 0。
求面积要分正负区间,或者对 $|f|$ 积分。
8 🧠 这跟认知科学有什么关系
这一节的对应关系是我验证过的(用代码实算),但心理学结论请对照文献。
心理物理函数和信号检测论 = 两个积分,认知科学最基础的两块砖。
8.1 心理物理函数:从"刺激强度"到"觉察概率"
问题:给一个强度为 $x$ 的刺激(声音多响、光多亮),被试"报告感觉到了"的概率是多少?
经典答案:这是累积正态分布
$$P(\text{觉察} \mid x) = \Phi\!\left(\frac{x - \mu}{\sigma}\right) = \int_{-\infty}^{(x-\mu)/\sigma}\frac{1}{\sqrt{2\pi}}e^{-t^2/2}\,dt$$
它是一个积分。这条 S 形曲线叫心理物理函数,是心理物理学的核心对象。
我实算了 $\Phi$ 的值(数值积分 vs scipy.stats.norm.cdf):
| $x$ | 数值积分 | 理论值 |
|---|---|---|
| $-1.96$ | $0.024998$ | $0.024998$ |
| $0$ | $0.500000$ | $0.500000$ |
| $1.0$ | $0.841345$ | $0.841345$ |
| $2.0$ | $0.977250$ | $0.977250$ |
完全吻合,你的心理物理实验数据点,理论上应该落在这条曲线上。
8.2 信号检测论:$d'$ 是两个积分之差
实验情境:被试要在"有信号"和"无信号"之间做判断。
两类错误不可避免:虚报(没信号说成有)和漏报(有信号说没)。
信号检测论(SDT)的核心量 $d'$(辨别力):
$$d' = z(\text{命中率}) - z(\text{虚报率})$$
其中 $z(\cdot)$ 是正态分布的分位数函数($\Phi$ 的反函数)。
我实算验证了这套关系:
| 真实 $d'$ | 命中率 | 虚报率 | 由命中/虚报反算的 $d'$ |
|---|---|---|---|
| $0.5$ | $0.5987$ | $0.4013$ | $0.5000$ ✓ |
| $1.0$ | $0.6915$ | $0.3085$ | $1.0000$ ✓ |
| $1.5$ | $0.7734$ | $0.2266$ | $1.5000$ ✓ |
| $2.0$ | $0.8413$ | $0.1587$ | $2.0000$ ✓ |
| $3.0$ | $0.9332$ | $0.0668$ | $3.0000$ ✓ |
验证完美闭合,说明"命中率/虚报率"和"$d'$"是同一组信息的两种表达。
8.3 别的积分(拓宽视野)
| 概念 | 积分形式 |
|---|---|
| 微分熵 | $h = -\int f(x)\ln f(x)\,dx$(正态时 $=\frac12\ln(2\pi e\sigma^2)$) |
| 信息量 / KL 散度 | $D_{KL} = \int p(x)\ln\frac{p(x)}{q(x)}dx$ |
| 反应时分布的期望 | $\int_0^\infty t f(t)dt$(阶段 11 讲过) |
| 贝叶斯后验 | 分母 $=\int p(D\mid\theta)p(\theta)d\theta$ |
| 神经调谐曲线下的面积 | $\int$ 神经反应 |
实算确认:正态分布 $\sigma=1$ 的微分熵,理论 $\frac12\ln(2\pi e) = 1.418939$,
数值积分 $-\int f\ln f\,dx = 1.418939$ ✓
⭐ 定积分在认知科学里是"把分布变成可测量数字"的工具。
概率、期望、熵、$d'$,都是用积分定义的。
你手算的部分在考试里;但在论文里,它们全都以 $\int$ 的形式出现。⚠️ 诚实说明:
- 上面的积分值、$\Phi$ 值、$d'$ 反算都是我实算的(可复现),
"心理物理函数是累积正态""$d'=z(H)-z(F)$"是SDT 的标准数学形式。
- 但"心理物理函数一定是对称正态吗"是领域内的研究问题
(Weibull、logistic、Gumbel 等都被用过),
"哪些条件下 SDT 假设成立"也有大量文献讨论。
请以 Signal Detection Theory and Psychophysics(Green & Swets)
和近年方法论文献为准。我保证数学,不保证模型选择。
9 数学作业 S12
- [ ] 1. 用牛顿-莱布尼茨公式求 $\int_0^2 (3x^2 + 2x)\,dx$
- [ ] 2. 求 $\int_0^1 x^3\,dx$ 和 $\int_{-1}^{1} x^3\,dx$(第二题想想能不能不算)
- [ ] 3. 求 $\int_1^4 \sqrt{x}\,dx$
- [ ] 4. 求 $\int_0^{\pi/2}\cos x\,dx$
- [ ] 5. 求 $\int_1^2 \dfrac{2}{x}\,dx$
- [ ] 6. 求 $y = x$ 与 $y = x^2$ 在 $[0,1]$ 之间围成的面积
- [ ] 7. 求 $f(x) = x^2$ 在 $[0,3]$ 上的平均值
- [ ] 8.(重点)求 $y = \sqrt{x}$ 绕 $x$ 轴旋转 $[0,1]$ 一周的体积
- [ ] 9.(重点)判断下列反常积分是否收敛,收敛的求值:
(a) $\int_1^\infty \frac{1}{x^3}dx$ (b) $\int_1^\infty \frac{1}{x}dx$ (c) $\int_0^1 \frac{1}{\sqrt{x}}dx$
- [ ] 10. 用变限积分求导求 $\dfrac{d}{dx}\int_0^x \sin(t^2)\,dt$
- [ ] 11.(Python)用 sympy 验证作业 1–9,并用 numpy 数值积分独立验证其中两题
第 11 题参考代码:
import numpy as np
import sympy as sp
from sympy import symbols, sin, cos, sqrt, pi, oo, simplify, integrate, diff, sympify
x = symbols('x', positive=True)
LOC = {'x': x}
def check(expr, lo, hi, name=""):
"""用符号积分算定积分,返回一个数。"""
e = sympify(expr, locals=LOC)
v = integrate(e, (x, lo, hi))
try:
num = float(v)
print(f"{name or expr:28s} = {str(v):12s} = {num:.6f}")
except TypeError:
print(f"{name or expr:28s} = {v} <- 发散或含符号")
return v
print("=== 符号积分 ===")
check("3*x**2 + 2*x", 0, 2, "∫_0^2 (3x²+2x)dx")
check("sqrt(x)", 1, 4, "∫_1^4 √x dx")
check("cos(x)", 0, pi / 2, "∫_0^{π/2} cos x dx")
check("2/x", 1, 2, "∫_1^2 2/x dx")
check("1/x**3", 1, oo, "∫_1^∞ 1/x³ dx")
check("1/x", 1, oo, "∫_1^∞ 1/x dx (发散)")
check("1/sqrt(x)", 0, 1, "∫_0^1 1/√x dx")
print()
print("=== 应用 ===")
check("x - x**2", 0, 1, "y=x 与 y=x² 之间面积")
print("平均值 =", integrate(x**2, (x, 0, 3)) / 3)
print("旋转体 V = π∫_0^1 (√x)² dx =", pi * integrate(x, (x, 0, 1)))
check("sin(x)", 0, 2 * pi, "∫_0^{2π} sin x dx (注意这是 0!)")
print()
print("=== 变限积分求导 ===")
t = symbols('t', positive=True)
F = integrate(sp.sin(t**2), (t, 0, x))
print("d/dx ∫_0^x sin(t²)dt =", simplify(diff(F, x)))
print()
print("=== numpy 数值积分独立验证 ===")
from scipy import integrate as si
# 验证 ∫_0^1 x² dx = 1/3
v1, err1 = si.quad(lambda z: z**2, 0, 1)
print(f"数值积分 ∫_0^1 x² dx = {v1:.10f} (误差 {err1:.2e}) 理论 1/3 = {1/3:.10f}")
# 验证 ∫_0^{π/2} cos x dx = 1
v2, err2 = si.quad(np.cos, 0, np.pi / 2)
print(f"数值积分 ∫_0^{{π/2}} cos = {v2:.10f} 理论 1")
# 黎曼和:用"数格子"的方式逼近,看它收敛
f = lambda z: z**2
for n in [10, 1000, 100000]:
xs = np.linspace(0, 1, n + 1)
left = np.sum(f(xs[:-1])) / n
right = np.sum(f(xs[1:])) / n
print(f" n={n:>6}: 左和={left:.6f} 右和={right:.6f}")
💡 三种验证方式并存:符号积分(sympy)、数值积分(scipy.quad)、黎曼和(numpy)。
学数学时能用代码从三个角度确认同一个答案,是最好的检查方式。
任何一个环节写错了,另两个都会对不上。
第二部分 · 编程
10 为什么需要"类"
先看一段你已经会写的代码(阶段 10 的元组方案):
subject = ("A01", 0.523, 0.95) # (编号, 反应时, 正确率)
print(subject[0], subject[1], subject[2])
问题来了:subject[0]、subject[1]、subject[2],这些数字是什么意思?
三个月后你还记得吗?别人读你的代码能看懂吗?
而且,数据和处理数据的函数是分开的:
def describe(subject):
return f"{subject[0]}: RT={subject[1]}, ACC={subject[2]}"
print(describe(subject))
痛点很明确:
| 问题 | 说明 |
|---|---|
| 靠位置记数据 | [0] 是编号还是反应时?靠脑记 |
| 数据和操作分家 | 数据在元组里,函数在外面,靠参数传 |
| 没法表达"属于谁" | 这个函数只对"被试元组"有意义,但看不出关联 |
"类"就是来解决这三件事的。
11 定义类与创建实例
class Subject:
"""一个被试的实验数据。"""
def __init__(self, sid, rt, acc):
self.sid = sid # 编号
self.rt = rt # 反应时
self.acc = acc # 正确率
def describe(self):
"""返回一句可读的说明。"""
return f"{self.sid}: RT={self.rt}, ACC={self.acc}"
# 创建"实例"
s1 = Subject("A01", 0.523, 0.95)
s2 = Subject("A02", 1.234, 0.80)
print(s1.rt) # 0.523 ← 用名字取,不用记位置!
print(s1.describe()) # A01: RT=0.523, ACC=0.95
print(s2.describe()) # A02: RT=1.234, ACC=0.8
对比一下:
| 元组方案 | 类方案 | |
|---|---|---|
| 取反应时 | subject[1](记位置) |
s1.rt(看名字) |
| 操作数据 | 函数在外面 describe(subject) |
方法挂在对象上 s1.describe() |
| 加字段 | 全体改索引,极易出错 | 加一行 self.xxx = ... |
⭐ 类把"数据"和"操作数据的函数"打包在一起。
这叫封装(encapsulation)。
12 __init__ 和 self
__init__ 是"初始化方法",创建实例时自动调用。
s1 = Subject("A01", 0.523, 0.95)
# ↑ 这里自动调用了 __init__(self=s1, sid="A01", rt=0.523, acc=0.95)
12.1 self 是什么(新手最大的困惑)
self 就是"这个实例自己"。
class Subject:
def __init__(self, sid, rt, acc):
self.sid = sid
# ↑ ↑
# 给"s1 的 sid 属性"赋值为参数 sid
s1 = Subject("A01", 0.523, 0.95)
s2 = Subject("A02", 1.234, 0.80)
print(s1.sid) # A01 ← s1 自己的
print(s2.sid) # A02 ← s2 自己的
关键理解:s1 和 s2 是两个独立的实例,各自有自己的一套属性。
⚠️
self不是 Python 的关键字,只是一个约定俗成的参数名。
你写成this也能跑,但千万别写,全世界都叫self。⚠️ 调用时不用传
self:
定义是def describe(self),调用是s1.describe()。
Python 自动把s1当作self传进去。这是新手最常犯的错:写
s1.describe(s1),多传了一个。
12.2 __init__ 里能做的事
不只是赋值,什么都能做(校验、计算、默认值):
class Subject:
"""一个被试的实验数据。"""
def __init__(self, sid, rt, acc=None):
if not sid:
raise ValueError("编号不能为空")
self.sid = sid
self.rt = float(rt)
self.acc = acc if acc is not None else 0.0
self.n_trials = 0 # 计数器,从 0 开始
def add_trial(self, correct):
"""记录一次试次。"""
self.n_trials += 1
return self
s = Subject("A01", 0.523)
s.add_trial(True).add_trial(False) # 可以链式调用
print(s.n_trials) # 2
print(s.acc) # 0.0
💡
raise ValueError(...)是"主动报错":数据不对时当场炸掉,
而不是让它带着错误数据往下跑。这叫"快速失败"(fail fast),是好的习惯。
13 实例属性 vs 类属性(一个经典的坑)
属性可以定义在两个地方:
class Subject:
species = "human" # ← 类属性:所有实例共享
def __init__(self, sid):
self.sid = sid # ← 实例属性:每个实例独有
s1 = Subject("A01")
s2 = Subject("A02")
print(s1.species, s2.species) # human human 都能读
print(s1.sid, s2.sid) # A01 A02 各自的
print(Subject.species) # human 类上也能读
区别:
| 实例属性 | 类属性 | |
|---|---|---|
| 定义位置 | __init__ 里,self.x = ... |
类里、方法外 |
| 归属 | 每个实例一份 | 所有实例共享一份 |
| 适合放什么 | 各自不同的数据(编号、反应时) | 全体一样的常量(类别、单位) |
13.1 ⚠️ 最大的坑:可变类属性
class Bad:
trials = [] # ⚠️ 可变对象当类属性!
def add(self, t):
self.trials.append(t) # 注意:这里改的是"共享的那个列表"
a = Bad()
b = Bad()
a.add(1)
print(a.trials) # [1]
print(b.trials) # [1] ← b 也被污染了!
这就是阶段 7 的"可变默认参数"坑的兄弟,同一个病。
正确写法:可变的东西永远在 __init__ 里创建。
class Good:
def __init__(self):
self.trials = [] # ✓ 每个实例一个自己的列表
def add(self, t):
self.trials.append(t)
a = Good()
b = Good()
a.add(1)
print(a.trials) # [1]
print(b.trials) # [] ← b 干净
⚠️ 铁律:类属性只放不可变的常量(字符串、数字、元组)。
列表、字典、集合永远放进__init__。这条规则和阶段 7 的"可变默认参数"是同一条,可变状态不能共享。
14 方法:挂在对象上的函数
方法就是"定义在类里的函数",第一个参数永远是 self。
class Subject:
def __init__(self, sid, rts):
self.sid = sid
self.rts = list(rts)
def mean_rt(self):
"""平均反应时。空列表返回 0。"""
if not self.rts:
return 0
return sum(self.rts) / len(self.rts)
def is_fast(self, threshold=0.8):
"""反应时是否快于阈值。"""
return self.mean_rt() < threshold
def __len__(self):
"""让 len(对象) 能工作。这是"魔术方法"。"""
return len(self.rts)
def __repr__(self):
"""让 print(对象) 输出可读内容,而不是 <object at 0x...>。"""
return f"Subject({self.sid!r}, n={len(self.rts)})"
s = Subject("A01", [0.5, 0.6, 0.7, 0.8])
print(s.mean_rt()) # 0.65
print(s.is_fast()) # True
print(len(s)) # 4 ← 调用了 __len__
print(s) # Subject('A01', n=4) ← 调用了 __repr__
💡
__len__和__repr__叫"魔术方法"(dunder methods)。
前后各两个下划线。它们让你的对象能用 Python 的内置语法:
len()、print()、+、==、for……全都靠魔术方法。阶段 13 会专门讲。
14.1 方法 vs 普通函数
| 普通函数 | 方法 | |
|---|---|---|
| 定义 | 文件里任意位置 | 类里面 |
| 第一个参数 | 随意 | 必须是 self |
| 调用 | describe(s) |
s.describe() |
| 能访问谁的数据 | 只有传入的参数 | 通过 self 访问整个对象 |
⚠️ 没有
self的"方法"不是方法:
def describe():写在类里,调用s.describe()会报
TypeError: describe() takes 0 positional arguments but 1 was given
因为 Python 硬塞了一个self进去,而它不接受。
15 🔧 技能卡 12:类与实例到底是什么关系
技能卡 11 教你拆文件;这张卡教你拆"数据和行为",理解类和实例,是读懂一切 Python 库的前提。
15.1 决策一:我该用类,还是用字典/元组?
| 情况 | 用什么 |
|---|---|
| 只是一组固定的数据,不常变 | 元组(不可变)或字典 |
| 数据 + 一堆专门操作它的函数 | 类 |
| 需要状态(比如计数器、累积结果) | 类 |
| 需要多个"同类但不同值"的东西 | 类 |
判断标准很简单:"我是不是总在做 xxx(data) 这样的调用?"
如果是,说明那些 xxx 应该变成 data.xxx()。
15.2 决策二:属性和方法该放哪?
class Subject: ← 类是"模具"
species = "human" ← 类属性:全体共享、不变的常量
def __init__(self, sid):
self.sid = sid ← 实例属性:每个实例独有
self.rts = [] ← ⚠️ 可变的一律放这里,不放类属性
def mean_rt(self): ← 方法:需要用到 self 的东西
return sum(self.rts) / len(self.rts)
15.3 决策三:怎么理解"类和实例"
最好的类比是"模具和零件":
| 概念 | 类比 | 代码 |
|---|---|---|
| 类 | 月饼模具 | class Subject: |
| 实例 | 压出来的每个月饼 | Subject("A01", ...) |
| 实例属性 | 每个月饼各自的馅 | self.sid |
| 类属性 | 模具的材质(所有月饼一样) | species = "human" |
| 方法 | 用模具能做的操作 | def mean_rt(self) |
一个类可以造出无限多个实例,每个属性值不同,但共享同一套方法。
验证一下:
class Subject:
"""一个被试。"""
def __init__(self, sid):
self.sid = sid
def mean_rt(self):
return 0.5
a = Subject("A01")
b = Subject("A02")
print(a.sid, b.sid) # A01 A02 属性各自独立
print(type(a) is type(b)) # True 都是同一个类
print(a is b) # False 但是两个不同的对象
# 方法这里有个反直觉的地方:
print(a.mean_rt is b.mean_rt) # False !每次访问"实例.方法"都会新建一个绑定方法对象
print(a.mean_rt is a.mean_rt) # False !连同一个实例的两次访问都不相等
print(a.mean_rt.__func__ is b.mean_rt.__func__) # True 底层函数确实只有一个
print(a.mean_rt.__func__ is Subject.mean_rt) # True 就是类里定义的那个
💡
a.mean_rt叫"绑定方法"(bound method):它是一个临时创建的小包装,
里面记住了两样东西:要调用的函数,和要当self传进去的a。
所以调用时不用再传self,它已经绑好了。这就是"面向对象"的字面含义:把行为绑定到对象上。
⚠️ 所以别用
is比较方法。要看"是不是同一个函数",
应该比较__func__,或者干脆用==(对同一个类的方法,==是 True)。
15.4 三条新手铁律
① 不可变的东西才放类属性
class A:
KIND = "被试" # ✓ 字符串,不可变
COUNT = 0 # ✓ 数字,不可变(但要注意 += 会创建新的)
items = [] # ✗ 列表!所有实例共享,会互相污染
② self 别忘了写
def mean_rt(self): # ✓
def mean_rt(): # ✗ 调用时报参数错误
③ 每个类都写一句 docstring
class Subject:
"""一个被试的实验数据:编号 + 各试次反应时。"""
⭐ 读别人的库时,
dir(obj)是你的探测器(阶段 2 的技能卡):
dir(s)列出所有属性和方法,立刻知道这个对象能干什么。
16 🧠 这跟认知科学有什么关系(编程侧)
概念性内容,无法用代码验证,请对照文献。
类 ↔ 认知科学里的"范畴"(category)和"图式"(schema),一个值得小心的类比。
16.1 编程侧的事实
类天然表达一种"类别—成员"结构:
| 编程 | 含义 |
|---|---|
Subject(类) |
一个类别:"被试"这个东西 |
Subject("A01")(实例) |
该类别下的一个具体成员 |
| 类属性 | 该类别的共同特征 |
| 实例属性 | 成员的个体差异 |
| 方法 | 该类事物能做的事 |
16.2 认知科学里的对应物
范畴化(categorization)是认知科学的核心议题之一:人怎么把无限多样的个体归入有限的类别?
| 编程概念 | 认知科学概念 | 关系 |
|---|---|---|
| 类 | 范畴 / 概念 | "鸟"是一个范畴 |
| 实例 | 成员 / 样例 | 具体的那只麻雀 |
| 类属性 | 共享特征 | 有羽毛、会飞 |
| 方法 | 该范畴事物的典型行为 | "鸟会飞" |
| 继承(阶段 13) | 层级分类 | "麻雀"是"鸟"的子类 |
不只是表面相似。面向对象编程的很多术语(class、instance、inheritance)就是从认知/逻辑学借来的词。
16.3 ⚠️ 但必须非常小心的地方
理论上的"类"和认知上的"范畴"有一个根本差别:
| 编程的类 | 认知的范畴 | |
|---|---|---|
| 边界 | 精确:要么是实例,要么不是 | 模糊:企鹅算不算"鸟"? |
| 判据 | 充要条件:满足条件就是 | 典型性:麻雀比企鹅"更像鸟" |
| 归属 | 二值:True/False | 程度:有些成员比其他成员更典型 |
这正是"经典范畴理论"(classical view)在认知科学里被推翻的原因:
人脑的范畴不像程序里的类那样有清晰边界。
后来的原型理论(Rosch)、样例理论、理论-理论都在试图解释
"为什么典型性效应会存在",而这是"类"模型无法解释的。
⚠️ 诚实说明:
- "面向对象的概念借自认知/逻辑学"是编程史的事实(Simula/Smalltalk 的设计者明确受影响)。
- 但"所以人脑就是面向对象的"是一个错误的推论。
程序里的类比能帮你快速建立直觉,但它绝不能代替认知科学的经验研究。
上面那张"边界/判据/归属"的对照表正说明了这一点。
- 关于范畴化,请以 Rosch (1975)、Murphy The Big Book of Concepts 等文献为准。
- 用熟悉的结构去理解陌生的结构(类比推理)本身是认知科学的研究对象。
这一节希望你先用它,再审视它。
17 编程作业 S12
- [ ] 1. 定义
class Subject,含sid、rt、acc三个属性,创建两个实例并打印 - [ ] 2. 给
Subject加方法describe(),返回一句可读的说明 - [ ] 3. 给
Subject加方法is_fast(threshold=0.8),判断rt是否小于阈值 - [ ] 4. 定义
class Trial,含condition(条件)、rt、correct,并加summary()方法 - [ ] 5. 解释并动手验证:为什么
def describe():(漏了self)会报错? - [ ] 6.(重点)下面代码有 bug,找出并修正(说清它和阶段 7 哪个坑同源):
class Bad: rts = [] def add(self, rt): self.rts.append(rt) a = Bad(); b = Bad() a.add(1) print(b.rts) # 期望 [],实际?- [ ] 7. 给
Subject加__repr__,让print(对象)输出可读内容 - [ ] 8. 给
Subject加__len__,返回试次数量 - [ ] 9.(技能)用
dir()查看一个实例,列出它的属性和方法;解释__init__为什么不在里面 - [ ] 10.(Python)写
class RTData,能add(rt)累积反应时,提供mean()、median()、n(),并带自测块
- [ ] 7. 给
第 6 题答案:
class Bad:
rts = [] # ⚠️ 可变对象当类属性:所有实例共享同一个列表
def add(self, rt):
self.rts.append(rt)
a = Bad(); b = Bad()
a.add(1)
print(b.rts) # [1] ← 不是 []!b 被 a 污染了
print(a.rts is b.rts) # True ← 证实:是同一个列表对象
修正版:
class Good:
def __init__(self):
self.rts = [] # ✓ 每个实例一个自己的列表
def add(self, rt):
self.rts.append(rt)
a = Good(); b = Good()
a.add(1)
print(a.rts, b.rts) # [1] []
print(a.rts is b.rts) # False ← 证实:各是各自的列表
同源坑:阶段 7 的"可变默认参数"
def add(x, lst=[])。
本质完全相同:默认值/类属性只在定义时创建一次,被所有调用/实例共享。
规律:可变对象不能出现在"只执行一次"的位置(默认参数、类属性、模块级变量)。
第 10 题参考写法:
"""被试反应时数据的最小容器。"""
class RTData:
"""累积一个被试的反应时,提供常用统计量。"""
def __init__(self, sid=""):
self.sid = sid
self.rts = [] # ✓ 可变属性在 __init__ 里创建
def add(self, rt):
"""记录一次反应时。非正数会被拒绝。"""
rt = float(rt)
if rt <= 0:
raise ValueError(f"反应时必须是正数,收到 {rt}")
self.rts.append(rt)
return self # 返回 self 以支持链式调用
def n(self):
"""试次数量。"""
return len(self.rts)
def mean(self):
"""平均反应时。无数据返回 0。"""
if not self.rts:
return 0
return sum(self.rts) / len(self.rts)
def median(self):
"""中位数反应时。无数据返回 0。"""
if not self.rts:
return 0
s = sorted(self.rts)
mid = len(s) // 2
if len(s) % 2 == 1:
return s[mid]
return (s[mid - 1] + s[mid]) / 2
def __len__(self):
return len(self.rts)
def __repr__(self):
return f"RTData({self.sid!r}, n={len(self.rts)}, mean={self.mean():.3f})"
if __name__ == "__main__":
d = RTData("A01")
d.add(0.5).add(0.6).add(0.7) # 链式调用
assert d.n() == 3
assert abs(d.mean() - 0.6) < 1e-9
assert abs(d.median() - 0.6) < 1e-9
assert len(d) == 3
print(d) # RTData('A01', n=3, mean=0.600)
# 空数据
e = RTData()
assert e.mean() == 0 and e.median() == 0 and e.n() == 0
# 非法输入要被拒绝
try:
d.add(-1)
raise AssertionError("应该报错但没报")
except ValueError:
pass # 预期行为
# 两个实例互不干扰(第 6 题那个坑的反面教材)
f = RTData("A02")
f.add(9.9)
assert d.n() == 3 and f.n() == 1, "实例之间串味了!"
print("全部检查用例通过")
本阶段自评
- 数学:定积分和不定积分的本质区别是什么?为什么定积分没有 $+C$?
- 数学:牛顿-莱布尼茨公式是什么?为什么它让定积分变得可算?
- 数学:$\int_0^{\pi}\sin x\,dx$ 怎么算?符号上最容易错在哪一步?
- 数学:为什么 $\int_{-1}^{1}x^3dx = 0$?什么情况下能这样"不算"?
- 数学:微积分基本定理为什么叫"基本"?它建立了什么关系?
- 数学:$\int_0^{2\pi}\sin x\,dx = 0$,但曲线与 $x$ 轴围成的面积是多少?为什么不同?
- 数学:$\int_1^\infty \frac{1}{x^p}dx$ 什么时候收敛?分界线在哪?
- 编程:类和元组比,解决了哪三个问题?
- 编程:
self是什么?为什么调用s.describe()时不用传它? - 编程:实例属性和类属性有什么区别?可变的东西为什么不能当类属性?
- 编程:
__repr__和__len__有什么用?它们为什么叫"魔术方法"? - 技能:什么时候该用类,什么时候元组/字典就够了?判断标准是什么?
- 连接:程序里的"类"和认知科学里的"范畴",有哪三个根本差别?为什么不能混为一谈?
全部答得上 → 进阶段 13。
有任何一个答不上 → 回去补对应小节,别跳。