▤书架 ← 返回索引
▶目录

阶段 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'(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 使用条件(三条,缺一不可)

  1. 必须是 $\frac00$ 或 $\frac{\infty}{\infty}$ 型(先验证!)
  2. $f,g$ 在去心邻域内可导,且 $g'(x)\ne 0$
  3. $\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

第 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

第 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 当成了局部变量,
而局部变量在使用前必须先赋值。


本阶段自评

  1. 数学:罗尔定理的三个条件分别防什么反例?
  2. 数学:拉格朗日定理的几何意义是什么?它和罗尔定理是什么关系?
  3. 数学:洛必达法则的三个使用条件是什么?哪一条最容易忽略?
  4. 数学:洛必达后极限不存在,能推出原极限不存在吗?举反例。
  5. 编程:默认值是在什么时候求值的?这导致什么问题?
  6. 编程:def f(lst=[]) 为什么危险?正确写法是什么?
  7. 编程:为什么要用 None 当哨兵,而不是判断 len(cart)==0?
  8. 技能:写检查用例时,应该测哪四类输入?为什么边界值最重要?
  9. 连接:可变默认参数的坑,在实验设计里对应什么现象?

全部答得上 → 进阶段 6。

最后更新:2026-09-26