阶段 05 讲义
数学 11h:高数 · 中值定理与洛必达
编程 9h:函数(二)· 可变默认参数的坑
第一部分 · 数学
1 罗尔定理:从直觉开始
先想一个画面:你早上从山脚出发,中午到山顶,傍晚又走回山脚。
问:这一天里,有没有某一刻,你恰好是水平移动的(既不向上也不向下)?
答案是"有",你总得从"上山"切换成"下山",在这中间必然有一瞬间是平的。
这就是罗尔定理。
罗尔定理:若 $f$ 满足
1. 在 $[a,b]$ 上连续
2. 在 $(a,b)$ 内可导
3. $f(a) = f(b)$(两端点一样高)则至少存在一点 $\xi \in (a,b)$,使 $f'(\xi) = 0$。
几何意义:两端等高,中间必有一处切线水平。
三个条件缺一不可,每一条都在防一种反例:
| 去掉哪条 | 反例 | 结果 |
|---|---|---|
| 不连续 | $f(x)=x$($x\in[0,1)$),$f(1)=0$ | 没有水平切线 |
| 不可导 | $f(x)=\lvert x\rvert$ 在 $[-1,1]$ | 尖点在 0,但 $f'(0)$ 不存在 |
| $f(a)\ne f(b)$ | $f(x)=x$ 在 $[0,1]$ | 导数恒为 1,永不为 0 |
1.1 完整例子
验证 $f(x)=x^2-4x+3$ 在 $[1,3]$ 上满足罗尔定理,并求 $\xi$。
第一步:检查条件
- 多项式,连续和可导都满足 ✓
- $f(1)=1-4+3=0$,$f(3)=9-12+3=0$ → $f(1)=f(3)$ ✓
第二步:求导、解方程
$$f'(x)=2x-4=0 \implies x=2$$
第三步:检查 $\xi$ 是否在开区间内
$\xi=2 \in (1,3)$ ✓
答:$\xi=2$,此时 $f(2)=-1$,切线水平。
2 拉格朗日中值定理 ⭐:罗尔的"斜过来"版
罗尔定理要求两端一样高。如果不一样高呢?
那就把整个图斜过来看,割线的斜率代替了"水平"。
拉格朗日中值定理:若 $f$ 在 $[a,b]$ 上连续、在 $(a,b)$ 内可导,
则至少存在一点 $\xi \in (a,b)$,使$$f'(\xi) = \frac{f(b)-f(a)}{b-a}$$
或写成更好用的形式:
$$f(b) - f(a) = f'(\xi)(b-a)$$
几何意义:曲线上至少有一点的切线,平行于端点连线。
$$f'(x_0) = \text{割线斜率}$$
凭什么成立:把罗尔定理"减掉割线"就得到了。
构造 $F(x) = f(x) - \left[f(a) + \frac{f(b)-f(a)}{b-a}(x-a)\right]$,
这是"$f$ 减去那条割线"。则 $F(a)=F(b)=0$,对 $F$ 用罗尔定理即可。
所以拉格朗日是罗尔的推论,这也是为什么两个定理长得像。
2.1 完整例子
验证 $f(x)=x^3$ 在 $[0,2]$ 上满足拉格朗日中值定理,并求 $\xi$。
第一步:算割线斜率
$$\frac{f(2)-f(0)}{2-0} = \frac{8-0}{2} = 4$$
第二步:求导,令它等于割线斜率
$$f'(x)=3x^2 = 4 \implies x^2 = \frac43 \implies x = \frac{2}{\sqrt3} \approx 1.1547$$
第三步:检查范围
$\xi=\frac{2}{\sqrt3} \approx 1.1547 \in (0,2)$ ✓
2.2 一个重要推论:「常函数」
若在区间内恒有 $f'(x)=0$,则 $f$ 是常函数。
为什么:任取两点 $x_1<x_2$,由拉格朗日:
$$f(x_2)-f(x_1) = f'(\xi)(x_2-x_1) = 0 \cdot (x_2-x_1) = 0$$
所以 $f(x_2)=f(x_1)$,即处处相等。
💡 这个推论看着无聊,却是解微分方程的基石。
后面证明"$f'=f$ 的解一定是 $Ce^x$"就靠它。
2.3 常用不等式:$\ln$ 的增量
对 $f(x)=\ln x$ 在 $[1,x]$ 上用拉格朗日($x>1$):
$$\ln x - \ln 1 = \frac{1}{\xi}(x-1), \quad \xi \in (1,x)$$
因为 $\xi>1$,所以 $\frac1\xi<1$,于是:
$$\ln x < x - 1 \quad (x>1)$$
数值验证:$x=e$ 时,$\ln e = 1 < e-1 \approx 1.718$ ✓
这类"用中值定理推不等式"是考试高频,也是分析里最基本的技巧。
3 洛必达法则 ⭐⭐
前面求 $\lim_{x\to0}\frac{\sin x}{x}$,我们靠"等价无穷小"和记忆。洛必达法则给了它一个通用套路。
3.1 适用形式
只管两种未定式:
$$\frac{0}{0} \qquad \text{和} \qquad \frac{\infty}{\infty}$$
这两种形式下:
$$\lim_{x\to a}\frac{f(x)}{g(x)} = \lim_{x\to a}\frac{f'(x)}{g'(x)}$$
人话:分子分母各自求导,再取极限。(不是商的求导法则!)
⚠️ 最常见的错误:把洛必达当成"商法则"。
洛必达是 $\frac{f'}{g'}$,分子求导、分母求导,各自求,
不是 $\left(\frac{f}{g}\right)'$。别写成 $\frac{f'g-fg'}{g^2}$。
3.2 使用条件(三条,缺一不可)
- 必须是 $\frac00$ 或 $\frac{\infty}{\infty}$ 型(先验证!)
- $f,g$ 在去心邻域内可导,且 $g'(x)\ne 0$
- $\lim\frac{f'}{g'}$ 存在(或为 $\infty$)
⚠️ 第 3 条最容易忽略。见下面"洛必达失效"。
3.3 例子 1:$\lim_{x\to0}\frac{\sin x}{x}$
验证类型:$\frac{\sin 0}{0}=\frac00$ ✓
$$\lim_{x\to0}\frac{\sin x}{x} = \lim_{x\to0}\frac{\cos x}{1} = \cos 0 = 1$$
注意这里 $\frac{d}{dx}(\sin x)=\cos x$,$\frac{d}{dx}(x)=1$。
顺带一提:这个极限本身就是 $\sin$ 的导数公式的来源,
所以某种意义上是"循环论证"。严谨的处理要用夹逼定理(阶段 2 做过)。
但作为计算技巧,这样用完全没问题。
3.4 例子 2:$\lim_{x\to0}\frac{e^x-1}{x}$
类型 $\frac00$ ✓
$$\lim_{x\to0}\frac{e^x-1}{x} = \lim_{x\to0}\frac{e^x}{1} = 1$$
和阶段 2 背的等价无穷小 $e^x-1\sim x$ 一致,那个结论就是这么来的。
3.5 例子 3:需要连续用两次
$$\lim_{x\to0}\frac{1-\cos x}{x^2} \qquad \left(\frac00\right)$$
第一次:
$$= \lim_{x\to0}\frac{\sin x}{2x} \qquad \left(\text{还是}\frac00\right)$$
第二次:
$$= \lim_{x\to0}\frac{\cos x}{2} = \frac12$$
验证:结果 $\frac12$,和阶段 2 背的 $1-\cos x\sim\frac{x^2}{2}$ 一致 ✓
3.6 例子 4:$\frac{\infty}{\infty}$ 型
$$\lim_{x\to\infty}\frac{\ln x}{x} \qquad \left(\frac{\infty}{\infty}\right)$$
$$= \lim_{x\to\infty}\frac{1/x}{1} = \lim_{x\to\infty}\frac1x = 0$$
💡 对数增长比线性增长慢得多。
算法里 $\log n$ 比 $n$ 好,就是这个意思。
3.7 例子 5:其它形式($0\cdot\infty$、$\infty-\infty$、$0^0$)
洛必达只管 $\frac00$ 和 $\frac\infty\infty$。遇到别的形式,先转化成这两种。
$0\cdot\infty$ 型:$\lim_{x\to0^+} x\ln x$
改写:$x\ln x = \dfrac{\ln x}{1/x}$(变成 $\frac{\infty}{\infty}$)
$$= \lim_{x\to0^+}\frac{1/x}{-1/x^2} = \lim_{x\to0^+}(-x) = 0$$
口诀:
| 原形式 | 怎么转 |
|---|---|
| $0\cdot\infty$ | 把其中一个写成倒数,挪到分母 |
| $\infty-\infty$ | 通分,或提取公因式 |
| $0^0$、$1^\infty$、$\infty^0$ | 取对数,变成 $0\cdot\infty$ |
3.8 ⚠️ 洛必达会失效:两个必须知道的例子
洛必达不是万能的。条件 3 不满足时,它会骗你。
3.8.1 失效例 A:求不出,但极限确实存在
$$\lim_{x\to\infty}\frac{x+\sin x}{x}$$
先看类型:$\frac{\infty}{\infty}$ ✓,形式上可以用。
用洛必达:
$$= \lim_{x\to\infty}\frac{1+\cos x}{1} = \lim_{x\to\infty}(1+\cos x)$$
这个极限不存在($1+\cos x$ 在 0 和 2 之间来回振荡)。
但原式的极限是存在的!直接约分就看得出来:
$$\frac{x+\sin x}{x} = 1 + \frac{\sin x}{x} \xrightarrow{x\to\infty} 1 + 0 = 1$$
数值验证:
| $x$ | 原式 | 洛必达后 |
|---|---|---|
| $10^2$ | 0.994936 | 1.862319 |
| $10^3$ | 1.000827 | 1.562379 |
| $10^4$ | 0.999969 | 0.047845 |
| $10^5$ | 1.000000 | 0.000639 |
原式稳稳趋于 1,洛必达后却在乱跳。
⚠️ 结论:洛必达后极限不存在,推不出原极限不存在。
洛必达只是"如果导数的比有极限,那么原式也有,且相等",
这是单向的。反过来不成立。
3.8.2 失效例 B:越用越绕回去
$$\lim_{x\to\infty}\frac{x}{\sqrt{x^2+1}} \qquad \left(\frac\infty\infty\right)$$
用一次洛必达:
$$= \lim_{x\to\infty}\frac{1}{\dfrac{x}{\sqrt{x^2+1}}} = \lim_{x\to\infty}\frac{\sqrt{x^2+1}}{x}$$
这正好是原式的倒数,再用一次洛必达就回到了起点。死循环。
正确做法:上下同除 $x$。
$$\frac{x}{\sqrt{x^2+1}} = \frac{1}{\sqrt{1+\frac{1}{x^2}}} \to \frac{1}{\sqrt{1+0}} = 1$$
💡 经验:洛必达之前先看看能不能约分/化简。
化简能解决的,就别上洛必达。
4 常见的坑
| 坑 | 说明 |
|---|---|
| 没验证类型就用 | 先确认是 $\frac00$ 或 $\frac\infty\infty$,否则结论无效 |
| 当成商法则 | 是 $\frac{f'}{g'}$,不是 $\frac{f'g-fg'}{g^2}$ |
| 导数比无极限就下结论 | 原极限可能仍存在(失效例 A) |
| 死用不停手 | 绕圈了或变复杂了,就停手换方法(失效例 B) |
| 忽略"$\xi$ 在开区间内" | 罗尔/拉格朗日的 $\xi\in(a,b)$,端点不算 |
5 🧠 这跟认知科学有什么关系
概念性内容,无法用代码验证,请对照文献。
中值定理 ↔ 心理物理函数的"平均斜率"和"瞬时斜率"。
做心理物理实验时,你会测一条心理测量函数:横轴是刺激强度,纵轴是"报告感觉到的概率"。用两个点连线的斜率(平均变化率)描述"这个区间里感受性提高了多少"。
中值定理告诉你:这个平均斜率,一定在某一点被瞬时斜率精确取到。
为什么这有用:如果你报告"平均斜率",审稿人会问"这是哪个强度上的斜率?",
中值定理保证了存在这样一个点,所以"平均斜率"是有明确意义的量,不是硬凑的。
洛必达的"未定式"↔ 心理量的 0/0 问题。
当刺激强度趋近于 0,而反应概率也趋近于 0 时,"每单位刺激带来的反应变化"就成了 $0/0$。
阈限的估计本质上就是在处理这类未定式,这也是为什么阈限估计要用整套心理物理方法(阶梯法、恒定刺激法),而不是简单相除。
⚠️ 诚实说明:这些是帮你建立直觉的类比,不是严格的数学对应。
中值定理是定理,心理物理函数是经验测量。请以文献为准。
6 数学作业 S5
- [ ] 1. 验证 $f(x)=x^2-2x$ 在 $[0,2]$ 上满足罗尔定理,并求出 $\xi$
- [ ] 2. 用拉格朗日中值定理证明:对任意 $x>0$,有 $\ln(1+x)<x$
- [ ] 3. 验证 $f(x)=\sqrt{x}$ 在 $[1,4]$ 上满足拉格朗日中值定理,求 $\xi$
- [ ] 4. 用洛必达求 $\lim_{x\to0}\dfrac{\tan x}{x}$
- [ ] 5. 用洛必达求 $\lim_{x\to0}\dfrac{e^x-1-x}{x^2}$(注意要连用两次)
- [ ] 6. 用洛必达求 $\lim_{x\to\infty}\dfrac{x^2}{e^x}$
- [ ] 7. 求 $\lim_{x\to0^+}x\ln x$($0\cdot\infty$ 型,先转化)
- [ ] 8.(重点)判断下面哪一步错了,并说出错在哪:
求 $\lim_{x\to\infty}\dfrac{x-\sin x}{x}$ 时,直接上洛必达得到
$\lim_{x\to\infty}\dfrac{1-\cos x}{1}$,因为该极限不存在,所以原极限不存在。提示:先用约分算原极限,再解释洛必达为什么在这里不好使。
- [ ] 9.(Python)用数值验证作业 8:对若干个越来越大的 $x$,分别打印原式和洛必达后的值,观察两者行为
第 9 题参考代码:
import numpy as np
for x in [10, 100, 1000, 10000, 100000]:
orig = (x - np.sin(x)) / x # 原式
lhop = 1 - np.cos(x) # 洛必达之后的式子
print(f"x={x:<8} 原式={orig:.10f} 洛必达后={lhop:.10f}")
你会看到:原式稳定趋近 1,而洛必达后的值一直在跳(因为 $\cos$ 在振荡)。
这就直观地说明了"洛必达后没有极限"不等于"原式没有极限"。
第二部分 · 编程
7 默认参数
默认参数让函数"有些参数可以不传"。
def greet(name, greeting="你好", punct="!"):
return f"{greeting},{name}{punct}"
print(greet("张三")) # 你好,张三!
print(greet("张三", "Hi")) # Hi,张三!
print(greet("张三", punct="。")) # 你好,张三。
规则:
| 谁 | 说明 |
|---|---|
| 不带默认值的 | 必须出现在前面 |
| 带默认值的 | 放在后面 |
def bad(a=1, b): # ❌ SyntaxError
return a + b
报错:SyntaxError: parameter without a default follows parameter with a default
💡 为什么:调用时按位置从左往右填。
如果a有默认值而b没有,f(5)到底是给a还是给b?没法判断。所以禁止。
8 默认值是在「定义时」算好的
这是理解本阶段那个大坑的关键。默认值在函数定义的那一刻算一次,之后一直复用,不是每次调用时重新算。
import time
def log(msg, t=time.time()): # ⚠️ t 在 def 时就固定了
return f"{t:.0f} {msg}"
print(log("第一次"))
time.sleep(1)
print(log("一秒后")) # 时间戳居然一样!
⚠️ 因为默认值只求值一次。想每次取当前时间,得用
t=None再在函数里判断。
这就是下面那个坑的同一种病因。
9 ⚠️⚠️ 可变默认参数的坑(本阶段重点)
这是 Python 最著名的坑之一,几乎每个人都会踩。
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("苹果")) # ['苹果']
print(add_item("香蕉")) # ['苹果', '香蕉'] ← 意外!
print(add_item("橙子")) # ['苹果', '香蕉', '橙子'] ← 越来越长!
明明每次只加了一个,为什么列表一直在累积?
9.1 为什么
因为 [] 这个默认值在 def 执行时就创建了一个列表对象,之后所有调用共用这一个。
回忆阶段 1 讲的:变量是标签。cart 这个标签一直贴在同一个列表上。
append 又是在原地修改(不新建对象),所以改动被保留了下来。
def show_default(lst=[]):
print(f"id={id(lst)} 内容={lst}")
lst.append(1)
show_default() # id=...5280 内容=[]
show_default() # id=...5280 内容=[1] ← 同一个对象
两次的 id 完全一样 → 铁证:是同一个列表。
💡 为什么会这样设计:因为默认值只求值一次(上一节)。
对不可变的默认值(数字、字符串、None)没问题,反正改不了。
但可变的(列表、字典、集合)就出事了。
9.2 正确写法:用 None 当哨兵
def add_item(item, cart=None):
if cart is None:
cart = [] # ← 每次调用都新建一个
cart.append(item)
return cart
print([add_item(i) for i in ["苹果", "香蕉", "橙子"]])
# [['苹果'], ['香蕉'], ['橙子']] ← 各自独立 ✓
为什么要用 None 而不是直接 cart=[] 再判断?原因是你判断不了。
如果默认值是 [],第一次调用时你怎么区分"用户没传"和"用户传了个空列表"?
None 是明确表示"没传"的哨兵值,和"空列表"区分得开。
⚠️ 什么时候要警惕:只要默认值是"可变的",就要警觉。
默认值 危险吗 def f(x=0)、def f(s="")、def f(b=None)✅ 安全(不可变) def f(lst=[])、def f(d={})、def f(s=set())⚠️ 危险
9.3 那如果我就是想"记住上次的结果"呢?
那就明确地写出来,别靠默认值偷偷实现:
_cache = [] # 明确声明:这是模块级共享状态
def add_item(item, cart=None):
if cart is None:
cart = _cache # 想共享就共享——但要让读代码的人看见
cart.append(item)
return cart
💡 经验:"意外的共享"是 bug,"显式的共享"是设计。
坑的可怕之处在于它没有报错,程序照跑,只是结果悄悄错了。
10 🔧 技能卡 5:给自己的函数写「检查用例」
技能卡 4 教你 assert。但 assert 检查什么,得你自己想。
这张卡讲一件区分"能跑"和"可信"的事:写完函数,自己给它出题。
10.1 什么叫检查用例
用"你已经知道答案的输入"去测函数。不用学 pytest,就用 assert:
def is_prime(n):
if n < 2:
return False
for d in range(2, int(n ** 0.5) + 1):
if n % d == 0:
return False
return True
# ---- 检查用例 ----
assert is_prime(2) == True # 最小的素数
assert is_prime(1) == False # 边界
assert is_prime(0) == False # 边界
assert is_prime(9) == False # 平方数
assert is_prime(97) == True # 素数
print("全部通过")
10.2 该测哪几类输入(这个清单很值钱)
| 类型 | 例子 | 为什么 |
|---|---|---|
| 正常值 | is_prime(97) |
基本功能 |
| 边界值 | 1、0、空列表、len==1 |
bug 高发区 |
| 异常值 | 负数、0、None |
看是崩溃还是合理处理 |
| 重复/极端 | 全相同元素、很大的数 | 隐藏假设 |
⚠️ 经验:边界值上出的 bug 比正常值多得多。
阶段 2 就提过"边界值是 bug 高发区",这里是它的具体做法。
10.3 为什么这习惯值钱
科研代码和数据分析的代码,正确性没法靠"跑起来没报错"判断。
一个算错统计量的脚本,跑起来一样安安静静地输出一个数。
只有你预先知道正确答案(用小手算的例子、用已知结论),才能发现它错了。
💡 这就是"可复现研究"的起点:你的代码应该带一套自己会检查自己的用例。
别人(和三个月后的你)拿到代码,跑一遍就知道有没有坏。
10.4 一个让步:数值别用 ==
阶段 1 讲过浮点数的坑。检查用例里比较浮点数要用容差:
import math
def my_sqrt(x):
return x ** 0.5
assert math.isclose(my_sqrt(2), 1.41421356237, rel_tol=1e-9)
assert math.isclose(my_sqrt(4), 2.0)
⚠️
assert my_sqrt(2) == 1.41421356237会失败,
浮点运算结果不是精确的那个数。用math.isclose,别用==。
11 🧠 这跟认知科学有什么关系(编程侧)
概念性内容,无法用代码验证,请对照文献。
可变默认参数的坑 ↔ 实验里的「顺序效应 / 遗留效应」(carryover effect)。
这是真的对应,而且很实用。
心理学实验里,如果被试做完条件 A 再做条件 B,条件 A 的残留影响会带进条件 B。
如果你不"重置状态",第二个条件的数据就被第一个污染了。
这和 def f(cart=[]) 是同一个病:
| 代码 | 实验 |
|---|---|
| 默认列表在调用之间被共用 | 被试的状态在条件之间没被重置 |
| 结果是列表越积越长 | 结果是性能被前面的条件系统性影响 |
| 没有报错,只是悄悄错了 | 数据看着正常,但结论是错的 |
实验设计里的对策,正好对应编程里的对策:
| 问题 | 编程的对策 | 实验的对策 |
|---|---|---|
| 状态被共用 | None 哨兵,每次新建 |
被试间设计(每人只做一个条件) |
| 顺序不可避免 | 显式声明共享 | 拉丁方平衡(counterbalance) |
| 想引入已知状态 | 显式传参 | 练习试次 / 洗脱期(washout) |
💡 "状态没重置"是一种系统性的、不报错的错误。
代码里它叫可变默认参数;实验里它叫遗留效应。
两种情况下,光看结果是看不出来的,这就是为什么要有方法论和检查用例。⚠️ 上表的对应关系是结构性类比,帮你记。实验设计的具体做法请以方法论教材为准。
12 编程作业 S5
- [ ] 1. 写
power(base, exp=2),默认求平方;power(3)得 9,power(3,3)得 27 - [ ] 2. 写
repeat(text, times=1, sep=" "),把文本重复若干次并用sep连接 -
[ ] 3.(重点)下面这个函数有 bug。先不跑,指出问题、说出原因,再改对:
def add_score(score, scores=[]): scores.append(score) return scores print(add_score(60)) print(add_score(70)) -
[ ] 4. 把第 3 题改成正确写法(
None哨兵),并写检查用例验证两次调用互不影响 - [ ] 5. 写
make_counter(),返回一个函数,每次调用返回值加 1提示:这要用闭包(阶段 4 的作用域),在函数里定义变量,内层函数用
nonlocal修改 - [ ] 6.(技能)给你阶段 4 写的
is_prime/factorial/find_max补检查用例,每类输入(正常 / 边界 / 异常)至少各一条
- [ ] 7. 用
time.time()演示"默认值只在定义时求值一次"
第 4 题参考写法:
import math
def add_score(score, scores=None):
if scores is None:
scores = []
scores.append(score)
return scores
# ---- 检查用例 ----
assert add_score(60) == [60]
assert add_score(70) == [70] # 关键:不被上一次污染
assert add_score(80, [1, 2]) == [1, 2, 80] # 显式传入时按传入的处理
# 显式传参时要能改到调用者的列表
shared = []
add_score(90, shared)
assert shared == [90]
print("全部通过")
第 5 题参考写法:
def make_counter():
count = 0
def next_value():
nonlocal count # 声明"我要改外层的 count"
count += 1
return count
return next_value
c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1()) # 1 2 3
print(c2()) # 1 ← 另一个独立的计数器
⚠️
nonlocal不能少:漏了会得到
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
因为 Python 看到函数里有count += 1,就把count当成了局部变量,
而局部变量在使用前必须先赋值。
本阶段自评
- 数学:罗尔定理的三个条件分别防什么反例?
- 数学:拉格朗日定理的几何意义是什么?它和罗尔定理是什么关系?
- 数学:洛必达法则的三个使用条件是什么?哪一条最容易忽略?
- 数学:洛必达后极限不存在,能推出原极限不存在吗?举反例。
- 编程:默认值是在什么时候求值的?这导致什么问题?
- 编程:
def f(lst=[])为什么危险?正确写法是什么? - 编程:为什么要用
None当哨兵,而不是判断len(cart)==0? - 技能:写检查用例时,应该测哪四类输入?为什么边界值最重要?
- 连接:可变默认参数的坑,在实验设计里对应什么现象?
全部答得上 → 进阶段 6。