用 SymPy 和 Manim 轻松搞定导数动画
大家好你有没有试过在Manim里做导数定义的动画就是那个经典的场景画一条曲线再画一条割线然后让割线上的一个点无限逼近另一个点最后变成切线。这个过程的核心是计算割线的斜率 (f(xh) - f(x)) / h并观察当 h 趋近于 0 时这个斜率是如何变化的。听起来很简单但实际操作起来手动去推导极限、计算每一帧的坐标不仅繁琐还特别容易出错。相信不少朋友都为此头疼过。想象一下我们要为函数 f(x)x3−2x1 做一个在 x1 处的切线动画。定义割线我们需要两个点P(1,f(1)) 和 Q(1h,f(1h))。计算斜率slope(f(1h)−f(1))/h。求极限为了让动画平滑过渡到切线我们需要知道当 h→0 时slope 的精确值也就是 f′(1)。动态更新在动画中h 是一个不断变小的值比如从 1 变到 0.01我们需要为每一个 h 实时计算 Q 点的坐标和割线的斜率。如果手动来做第2、3步就需要展开 (1h)3−2(1h)1再减去 f(1)化简最后求极限。对于复杂的函数这简直是灾难比如函数f(x)sin(x2)而且在代码里硬编码这些公式一旦函数变了所有计算都得重来。这就是我们的痛点动态、精准、自动化地处理符号计算。SymPy 解决方案让计算机做数学SymPy正是解决这个问题的完美工具它可以把x,h当作真正的数学符号来处理而不是具体的数字。针对我们的需求只需要两个核心函数diff(f, x): 自动求导。直接告诉我们f(x)的导函数是什么。limit(expr, h, 0): 计算极限。可以验证我们的割线斜率在h-0时的确等于导数值。下面看一段核心的SymPy代码感受一下它的威力span stylecolor:#000000span stylebackground-color:#ffffffcode classlanguage-pythonspan stylecolor:#0000fffrom/span sympy span stylecolor:#0000ffimport/span symbols, diff, limit span stylecolor:#008000# 定义符号变量/span x symbols(span stylecolor:#a31515x/span) span stylecolor:#008000# 定义我们的函数 f(x)/span f x**span stylecolor:#8800003/span - span stylecolor:#8800002/span*x span stylecolor:#8800001/span span stylecolor:#008000# --- 核心操作 ---/span span stylecolor:#008000# 自动求导得到 f(x)/span f_prime diff(f, x) span stylecolor:#0000ffprint/span(span stylecolor:#a31515f导函数 f(x) {f_prime}/span) span stylecolor:#008000# 输出: 导函数 f(x) 3*x**2 - 2/span span stylecolor:#008000# 在 x1 处的导数值/span slope_at_1 f_prime.subs(x, span stylecolor:#8800001/span) span stylecolor:#0000ffprint/span(span stylecolor:#a31515fx1 处的瞬时变化率 (斜率) {slope_at_1}/span) span stylecolor:#008000# 输出: x1 处的瞬时变化率 (斜率) 1/span span stylecolor:#008000# 用极限来验证割线斜率/span span stylecolor:#008000# 割线斜率表达式/span secant_slope_expr (f.subs(x, span stylecolor:#8800001/spanh) - f.subs(x, span stylecolor:#8800001/span)) / h span stylecolor:#008000# 计算 h-0 时的极限/span limit_slope limit(secant_slope_expr, h, span stylecolor:#8800000/span) span stylecolor:#0000ffprint/span(span stylecolor:#a31515f通过极限计算得到的斜率 {limit_slope}/span) span stylecolor:#008000# 输出: 通过极限计算得到的斜率 1/span /code/span/span看我们完全不用关心中间复杂的代数运算SymPy几行代码就帮我们完成了求导和极限验证并且结果精确无误。这为我们接下来的Manim动画提供了坚实的数学基础。Manim 联动实战让切线“动”起来现在我们将SymPy的计算能力嵌入到Manim动画中。我们将使用ValueTracker来控制h的值让它从一个较大的数如1逐渐减小到接近0。在每一帧Manim都会调用SymPy重新计算Q点的位置和割线从而实现动态效果。下面是核心的代码span stylecolor:#000000span stylebackground-color:#ffffffcode classlanguage-pythonspan stylecolor:#0000fffrom/span manim span stylecolor:#0000ffimport/span * span stylecolor:#0000fffrom/span sympy span stylecolor:#0000ffimport/span symbols, lambdify, diff span stylecolor:#0000ffclass/span span stylecolor:#a31515DerivativeAnimation/span(span stylecolor:#a31515Scene/span): span stylecolor:#0000ffdef/span span stylecolor:#a31515construct/span(self): span stylecolor:#008000# SymPy 符号计算部分 /span x_sym symbols(span stylecolor:#a31515x/span) f_sym x_sym**span stylecolor:#8800003/span - span stylecolor:#8800002/span*x_sym span stylecolor:#8800001/span span stylecolor:#008000# 原函数f(x) x³ - 2x 1/span f lambdify(x_sym, f_sym, span stylecolor:#a31515numpy/span) span stylecolor:#008000# 转为 NumPy 函数供绘图/span f_prime_sym diff(f_sym, x_sym) span stylecolor:#008000# SymPy 自动求导f(x) 3x² - 2/span x_p span stylecolor:#8800001/span span stylecolor:#008000# 切点横坐标/span exact_k span stylecolor:#0000fffloat/span(f_prime_sym.subs(x_sym, x_p)) span stylecolor:#008000# 精确导数 f(1) 1/span span stylecolor:#008000# Manim 坐标系与曲线 /span ax Axes(x_range[-span stylecolor:#8800002/span, span stylecolor:#8800003/span], y_range[-span stylecolor:#8800003/span, span stylecolor:#8800005/span]) graph ax.plot(f, colorYELLOW) span stylecolor:#008000# 原函数曲线/span p_point Dot(ax.c2p(x_p, f(x_p)), colorRED) span stylecolor:#008000# 切点 P/span span stylecolor:#008000# ValueTracker 驱动割线动态逼近 /span h_tracker ValueTracker(span stylecolor:#8800001/span) span stylecolor:#008000# h 从 1 逐渐减小到 0.001/span span stylecolor:#008000# 割线随 h 变化而重新绘制/span span stylecolor:#0000ffdef/span span stylecolor:#a31515get_secant_line/span(): h_val h_tracker.get_value() x_q x_p h_val k (f(x_q) - f(x_p)) / h_val span stylecolor:#008000# 割线斜率 Δy/Δx/span span stylecolor:#0000ffreturn/span ax.plot( span stylecolor:#0000fflambda/span x: k * (x - x_p) f(x_p), span stylecolor:#008000# 点斜式/span colorGREEN, x_range[x_p - span stylecolor:#8800001/span, x_q span stylecolor:#8800001/span] ) secant_line always_redraw(get_secant_line) span stylecolor:#008000# 切线使用 SymPy 算出的精确导数/span tangent_line ax.plot( span stylecolor:#0000fflambda/span x: exact_k * (x - x_p) f(x_p), colorPURPLE, x_range[-span stylecolor:#8800000.5/span, span stylecolor:#8800002.5/span] ) span stylecolor:#008000# 动画流程 /span self.play(Create(ax), Create(graph), Create(p_point)) self.play(Create(secant_line)) span stylecolor:#008000# 核心h → 0割线动态逼近切线/span self.play( h_tracker.animate.set_value(span stylecolor:#8800000.001/span), run_timespan stylecolor:#8800005/span, rate_funcrate_functions.ease_in_out_quad, ) span stylecolor:#008000# 对比展示精确切线/span self.play(Create(tangent_line)) self.wait(span stylecolor:#8800001/span) /code/span/span代码关键点解析lambdify: 连接SymPy和Manim的桥梁。它把SymPy的符号表达式f_sym转换成一个普通的Python函数f这个函数可以接受NumPy数组作为输入正好符合 Manimax.plot()的要求。ValueTracker:Manim中创建动态效果的核心。h_tracker存储了h的当前值。always_redraw: 这个装饰器告诉Manim被它修饰的对象如q_point和secant_line需要在每一帧都重新计算和绘制。它们内部的函数get_q_point和get_secant_line会读取h_tracker的最新值并调用f函数来获取最新的坐标。动态割线: 在get_secant_line中我们虽然可以直接用两点式画线但这里展示了如何利用SymPy的思想——通过计算斜率和截距来定义直线逻辑更清晰。效果展示说明运行这段代码你会看到以下动画效果坐标系与函数登场黄色的三次函数f(x)x3−2x1被绘制出来。固定点 P在x1处一个红色的点P被标记出来。动态点 Q 与割线一个蓝色的点Q出现在P的右侧因为初始h1一条绿色的割线连接P和Q。魔法时刻动画开始Q点开始平滑地向P点移动h值从 1.5 逐渐减小到 0.01。与此同时绿色的割线也随之旋转。切线显现当Q无限接近P时割线几乎不再变化。此时一条紫色的精确切线被绘制出来你会发现它和最终的割线几乎完全重合整个过程直观地展示了导数作为瞬时变化率的几何意义而这一切的精准性都由SymPy在幕后保证。