跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python算法

Python 数学可视化实战:显函数、隐函数与曲线交互绘图

使用 Python Tkinter 和 Matplotlib 构建交互式数学可视化工具,支持显函数、隐函数及物理场分布绘制。通过安全表达式解析防止注入,实现动态更新与图像保存功能,适用于教学演示与科学计算场景。

灵魂伴侣发布于 2026/3/16更新于 2026/6/1125 浏览
Python 数学可视化实战:显函数、隐函数与曲线交互绘图

Python 数学可视化实战:显函数、隐函数与复杂曲线的交互式绘图

在科学计算和数据分析中,将抽象的函数关系转化为直观的图形是理解物理现象的关键。本文基于 Python 的 Tkinter 和 Matplotlib 库,构建一个功能完善的函数可视化工具,支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图。

核心架构与安全机制

界面与计算分层

系统采用经典的 MVC 思想进行分层:

  • 界面层:使用 Tkinter 搭建 GUI,提供类型选择、表达式输入及预设函数菜单。
  • 计算层:负责数值运算。显函数通过 np.linspace 采样点计算;隐函数利用等高线算法 contour 绘制等值线。
  • 安全层:这是最关键的一环。直接解析用户输入的字符串存在代码注入风险。我们采用白名单过滤非法字符,并限制可执行的函数范围(仅允许 numpy 标准数学函数),确保运行环境的安全。

安全表达式解析

在接收用户输入后,必须先验证表达式的合法性。我们不仅检查字符集,还校验括号匹配,防止语法错误导致的崩溃。

def is_valid_expression(expr):
    """验证表达式安全性"""
    allowed_chars = set("0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ ")
    invalid_chars = set(expr.replace('.', '').replace('_', '')) - allowed_chars
    if invalid_chars:
        raise ValueError(f"非法字符:{''.join(invalid_chars)}")
    # 括号匹配检查
    stack = []
    for char in expr:
        if char == '(': stack.append(char)
        elif char == ')':
            if not stack: raise ValueError("括号不匹配")
            stack.pop()
    if stack: raise ValueError("括号不匹配")
    return True

 ():
    
    expr = expr.replace(, )  
    allowed_funcs = {
        : np, : np.sin, : np.cos, : np.tan,
        : np.exp, : np.sqrt, : np.log, : np.pi
    }
    safe_globals = {: }
    safe_locals = {**allowed_funcs, **namespace}
    compiled_code = (expr, , )
     (compiled_code, safe_globals, safe_locals)
def
safe_eval
expr, namespace
"""安全执行表达式"""
'^'
'**'
# 替换幂运算符
'np'
'sin'
'cos'
'tan'
'exp'
'sqrt'
'log'
'pi'
"__builtins__"
None
compile
'<string>'
'eval'
return
eval

这里要注意,safe_eval 中禁用了 __builtins__,这是防止恶意调用系统函数的关键步骤。

显函数可视化

对于形如 $y = f(x)$ 的显函数,实现相对直接。我们需要生成一组 x 坐标,然后逐个或向量化计算对应的 y 值。

核心实现逻辑

def plot_explicit_function(self, f, x_range, title):
    """绘制显函数"""
    self.fig.clear()
    ax = self.fig.add_subplot(111)
    ax.set_facecolor('white')
    x = np.linspace(x_range[0], x_range[1], 1000)
    # 逐点计算防止数组维度错误,同时保证安全
    y = np.array([f(xi) for xi in x])
    ax.plot(x, y, 'b-', linewidth=2.5)
    ax.set_title(title)
    ax.grid(True, linestyle='--', alpha=0.6)
    self.optimize_ticks(ax, x_range, (y.min(), y.max()))

实际运行时,如果函数包含除零操作(如 $1/x$ 在 $x=0$ 处),Matplotlib 会自动处理 NaN 值,但最好在前端提示用户注意定义域。

案例演示

  • 三次函数:展示多项式曲线的形态变化。
  • 双曲线:输入 1/x 即可快速查看渐近线特征。

隐函数可视化

隐函数 $F(x, y) = 0$ 无法直接表示为 $y=f(x)$,通常需要通过网格扫描和等高线技术来绘制。

核心实现逻辑

def plot_implicit_equation(self, eq, x_range, y_range):
    """绘制隐函数 F(x,y)=0"""
    x = np.linspace(x_range[0], x_range[1], 500)
    y = np.linspace(y_range[0], y_range[1], 500)
    X, Y = np.meshgrid(x, y)
    Z = eq(X, Y)
    # 绘制零等高线
    self.fig.contour(X, Y, Z, levels=[0], colors='red', linewidths=2.5)
    self.fig.contourf(X, Y, Z, alpha=0.6)  # 填充色显示数值分布
    self.fig.colorbar(label='F(x,y)')

这里使用了 meshgrid 生成二维网格,然后计算每个点的函数值。当值为 0 时即为曲线位置。

案例演示

  • 圆方程:$x^2 + y^2 = 4$。
  • 笛卡尔叶形线:$x^3 + y^3 - 3xy = 0$,这是一个经典的代数曲线。

特色曲线与物理应用

心形线(数学艺术)

利用隐函数公式 $(x^2+y^2-1)^3 - x^2y^3 = 0$ 可以绘制出浪漫的心形图案。我们在绘制时增加了粉色填充,使其更具视觉吸引力。

电势分布(物理应用)

在物理教学中,可视化电场分布非常有用。我们可以模拟两个点电荷的电势叠加:

def plot_electric_potential(self):
    charges = [{"x": -1, "y": 0, "q": 1}, {"x": 1, "y": 0, "q": -1}]
    x = np.linspace(-2.5, 2.5, 500)
    y = np.linspace(-2, 2, 500)
    X, Y = np.meshgrid(x, y)
    V = sum(charge['q'] / np.sqrt((X - c['x'])**2 + (Y - c['y'])**2) for c in charges)
    self.fig.contourf(X, Y, V, cmap='coolwarm')
    # 标记电荷位置
    for charge in charges:
        color = 'red' if charge['q'] > 0 else 'blue'
        marker = '+' if charge['q'] > 0 else '_'
        self.fig.scatter([charge['x']], [charge['y']], s=300, c=[color], marker=marker)

这种热力图能直观展示正负电荷之间的电势梯度变化。

交互式 GUI 设计

为了让工具更实用,我们集成了 Tkinter 作为前端框架。

界面布局

左侧放置控制面板,包含单选按钮切换函数类型,下拉菜单选择预设函数,以及输入框填写自定义表达式。右侧则是 Matplotlib 的画布区域,自带缩放和平移工具条。

动态控件更新

根据用户选择的类型(显函数/隐函数/心形线),程序会动态显示或隐藏对应的输入面板,避免界面混乱。

完整代码实现

以下是整合后的完整代码,可直接运行。请注意,首次运行可能需要安装依赖库:pip install matplotlib numpy。

import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import re
import os

# 设置 matplotlib 支持中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC", "Arial Unicode MS"]
plt.rcParams["axes.unicode_minus"] = False

class FunctionVisualizer:
    def __init__(self, root):
        self.root = root
        self.root.title("函数与方程可视化工具")
        self.root.geometry("1200x800")
        self.root.minsize(1000, 700)
        
        # 预设函数分组
        self.explicit_presets = {
            "三次函数": {"func": lambda x: x**3 - 3*x, "expr": "x**3 - 3*x", "x_range": (-2.5, 2.5), "title": "三次函数:$y = x^3 - 3x$"},
            "双曲线": {"func": lambda x: 1/x, "expr": "1/x", "x_range": (-5, 5), "title": "双曲线:$y = \frac{1}{x}$"},
            "指数函数": {"func": lambda x: np.exp(x), "expr": "np.exp(x)", "x_range": (-3, 3), "title": "指数函数:$y = e^x$"},
        }
        self.implicit_presets = {
            "圆": {"eq": lambda x, y: x**2 + y**2 - 4, "expr": "x**2 + y**2 - 4", "x_range": (-3, 3), "y_range": (-3, 3), "title": "圆:$x^2 + y^2 = 4$"},
            "椭圆": {"eq": lambda x, y: x**2/4 + y**2/9 - 1, "expr": "x**2/4 + y**2/9 - 1", "x_range": (-3, 3), "y_range": (-4, 4), "title": "椭圆:$\frac{x^2}{4} + \frac{y^2}{9} = 1$"},
            "笛卡尔叶形线": {"eq": lambda x, y: x**3 + y**3 - 3*x*y, "expr": "x**3 + y**3 - 3*x*y", "x_range": (-3, 3), "y_range": (-3, 3), "title": "笛卡尔叶形线:$x^3 + y^3 = 3xy$"},
        }

        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)

        left_frame = ttk.LabelFrame(main_frame, text="函数与方程可视化选项", padding=10, width=375)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
        left_frame.pack_propagate(False)

        right_frame = ttk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        self.plot_frame = ttk.Frame(right_frame)
        self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        self.fig = Figure(figsize=(8, 6), dpi=100)
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        self.toolbar_frame = ttk.Frame(right_frame, height=40)
        self.toolbar_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
        self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
        self.toolbar.update()

        self.create_controls(left_frame)
        self.plot_predefined_function()

    def create_controls(self, parent):
        ttk.Label(parent, text="选择可视化类型:", font=("SimHei", 10, "bold")).pack(anchor=tk.W, pady=(0, 10))
        
        self.viz_type = tk.StringVar(value="explicit")
        types = [("显函数", "explicit"), ("隐函数", "implicit"), ("心形线", "heart"), ("电势分布", "potential")]
        for text, value in types:
            ttk.Radiobutton(parent, text=text, variable=self.viz_type, value=value, command=self.update_controls).pack(anchor=tk.W, padx=5, pady=2)

        self.preset_frame = ttk.LabelFrame(parent, text="预设函数", padding=10)
        self.preset_frame.pack(fill=tk.X, pady=10)
        
        self.preset_functions = tk.StringVar()
        self.preset_combobox = ttk.Combobox(self.preset_frame, textvariable=self.preset_functions, width=30)
        self.preset_combobox.pack(fill=tk.X, pady=5)
        
        ttk.Button(self.preset_frame, text="绘制预设函数", command=self.plot_predefined_function).pack(fill=tk.X, pady=5)

        self.explicit_frame = ttk.LabelFrame(parent, text="显函数输入", padding=10)
        self.explicit_frame.pack(fill=tk.X, pady=10)
        ttk.Label(self.explicit_frame, text="函数表达式 (例如 x**2):").pack(anchor=tk.W)
        self.explicit_entry = ttk.Entry(self.explicit_frame, width=30)
        self.explicit_entry.insert(0, "x**3 - 3*x")
        self.explicit_entry.pack(fill=tk.X, pady=5)
        ttk.Label(self.explicit_frame, text="X 范围 (min,max):").pack(anchor=tk.W)
        self.x_range_entry = ttk.Entry(self.explicit_frame, width=30)
        self.x_range_entry.insert(0, "-2.5,2.5")
        self.x_range_entry.pack(fill=tk.X, pady=5)
        ttk.Button(self.explicit_frame, text="绘制显函数", command=self.plot_explicit).pack(fill=tk.X, pady=5)

        self.implicit_frame = ttk.LabelFrame(parent, text="隐函数输入", padding=10)
        self.implicit_frame.pack(fill=tk.X, pady=10)
        ttk.Label(self.implicit_frame, text="方程表达式 (例如 x**2 + y**2 - 4):").pack(anchor=tk.W)
        self.implicit_entry = ttk.Entry(self.implicit_frame, width=30)
        self.implicit_entry.insert(0, "x**3 + y**3 - 3*x*y")
        self.implicit_entry.pack(fill=tk.X, pady=5)
        ttk.Label(self.implicit_frame, text="X 范围 (min,max):").pack(anchor=tk.W)
        self.implicit_x_range_entry = ttk.Entry(self.implicit_frame, width=30)
        self.implicit_x_range_entry.insert(0, "-3,3")
        self.implicit_x_range_entry.pack(fill=tk.X, pady=5)
        ttk.Label(self.implicit_frame, text="Y 范围 (min,max):").pack(anchor=tk.W)
        self.implicit_y_range_entry = ttk.Entry(self.implicit_frame, width=30)
        self.implicit_y_range_entry.insert(0, "-3,3")
        self.implicit_y_range_entry.pack(fill=tk.X, pady=5)
        ttk.Button(self.implicit_frame, text="绘制隐函数", command=self.plot_implicit).pack(fill=tk.X, pady=5)

        ttk.Button(parent, text="保存图像", command=self.save_image).pack(side=tk.BOTTOM, pady=10)
        self.update_controls()

    def update_controls(self):
        viz_type = self.viz_type.get()
        self.preset_frame.pack_forget()
        self.explicit_frame.pack_forget()
        self.implicit_frame.pack_forget()
        
        if viz_type == "explicit":
            self.explicit_frame.pack(fill=tk.X, pady=10)
            self.update_preset_options(self.explicit_presets.keys())
        elif viz_type == "implicit":
            self.implicit_frame.pack(fill=tk.X, pady=10)
            self.update_preset_options(self.implicit_presets.keys())
        elif viz_type == "heart":
            self.plot_heart_curve()
        elif viz_type == "potential":
            self.plot_electric_potential()
        
        self.preset_frame.pack(fill=tk.X, pady=10)

    def update_preset_options(self, options=None):
        if options is None: options = []
        self.preset_combobox["values"] = list(options)
        if options:
            self.preset_functions.set(list(options)[0])

    def plot_predefined_function(self):
        viz_type = self.viz_type.get()
        selected = self.preset_functions.get()
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_facecolor("white")
        self.fig.set_facecolor("white")
        
        if viz_type == "explicit" and selected in self.explicit_presets:
            data = self.explicit_presets[selected]
            self.plot_explicit_function(f=data["func"], x_range=data["x_range"], title=data["title"])
            self.explicit_entry.delete(0, tk.END)
            self.explicit_entry.insert(0, data["expr"])
            self.x_range_entry.delete(0, tk.END)
            self.x_range_entry.insert(0, f"{data['x_range'][0]},{data['x_range'][1]}")
        elif viz_type == "implicit" and selected in self.implicit_presets:
            data = self.implicit_presets[selected]
            self.plot_implicit_equation(eq=data["eq"], x_range=data["x_range"], y_range=data["y_range"], title=data["title"])
            self.implicit_entry.delete(0, tk.END)
            self.implicit_entry.insert(0, data["expr"])
            self.implicit_x_range_entry.delete(0, tk.END)
            self.implicit_x_range_entry.insert(0, f"{data['x_range'][0]},{data['x_range'][1]}")
            self.implicit_y_range_entry.delete(0, tk.END)
            self.implicit_y_range_entry.insert(0, f"{data['y_range'][0]},{data['y_range'][1]}")
        self.canvas.draw()

    def is_valid_expression(self, expr):
        allowed_chars = set("0123456789.+-*/()xy^np_sin_cos_tan_exp_sqrt_log_pi_ ")
        cleaned = expr.replace('.', '').replace('_', '')
        invalid_chars = set(cleaned) - allowed_chars
        if invalid_chars:
            raise ValueError(f"非法字符:{''.join(invalid_chars)}")
        stack = []
        for char in expr:
            if char == '(': stack.append(char)
            elif char == ')':
                if not stack: raise ValueError("括号不匹配:缺少左括号")
                stack.pop()
        if stack: raise ValueError("括号不匹配:缺少右括号")
        return True

    def safe_eval(self, expr, namespace):
        try:
            self.is_valid_expression(expr)
            expr = expr.replace('^', '**')
            allowed_funcs = {'np': np, 'sin': np.sin, 'cos': np.cos, 'tan': np.tan, 'exp': np.exp, 'sqrt': np.sqrt, 'log': np.log, 'pi': np.pi, 'arctan2': np.arctan2}
            safe_globals = {"__builtins__": None}
            safe_locals = {**allowed_funcs, **namespace}
            compiled_code = compile(expr, '<string>', 'eval')
            return eval(compiled_code, safe_globals, safe_locals)
        except Exception as e:
            raise ValueError(f"表达式错误:{str(e)}")

    def plot_explicit(self):
        try:
            func_str = self.explicit_entry.get().strip()
            x_range_str = self.x_range_entry.get().strip()
            if not func_str or not x_range_str:
                raise ValueError("请输入函数表达式和 X 范围")
            x_min, x_max = map(float, x_range_str.split(","))
            if x_min >= x_max:
                raise ValueError("X 范围的最小值必须小于最大值")
            x_vals = np.linspace(x_min, x_max, 1000)
            y_vals = np.zeros_like(x_vals)
            for i, x in enumerate(x_vals):
                y_vals[i] = self.safe_eval(func_str, {'x': x})
            self.plot_explicit_function(f=lambda x: y_vals, x_range=(x_min, x_max), title=f"显函数:$y = {self.get_function_label(func_str)}$")
            self.canvas.draw()
        except Exception as e:
            messagebox.showerror("错误", f"绘制显函数时出错:{str(e)}")

    def plot_implicit(self):
        try:
            eq_str = self.implicit_entry.get().strip()
            x_range_str = self.implicit_x_range_entry.get().strip()
            y_range_str = self.implicit_y_range_entry.get().strip()
            if not eq_str or not x_range_str or not y_range_str:
                raise ValueError("请输入完整的方程表达式和范围")
            x_min, x_max = map(float, x_range_str.split(","))
            y_min, y_max = map(float, y_range_str.split(","))
            if x_min >= x_max or y_min >= y_max:
                raise ValueError("范围的最小值必须小于最大值")
            eq = lambda X, Y: self.safe_eval(eq_str, {'x': X, 'y': Y})
            self.plot_implicit_equation(eq=eq, x_range=(x_min, x_max), y_range=(y_min, y_max), title=f"隐函数:${self.get_function_label(eq_str)} = 0$")
            self.canvas.draw()
        except Exception as e:
            messagebox.showerror("错误", f"绘制隐函数时出错:{str(e)}")

    def plot_explicit_function(self, f, x_range=(-5, 5), title="显函数图像"):
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_facecolor("white")
        self.fig.set_facecolor("white")
        ax.grid(True, linestyle="--", alpha=0.6)
        ax.spines["left"].set_position("zero")
        ax.spines["bottom"].set_position("zero")
        ax.spines["right"].set_visible(False)
        ax.spines["top"].set_visible(False)
        x = np.linspace(x_range[0], x_range[1], 1000)
        try:
            y = f(x)
        except Exception as e:
            messagebox.showerror("函数错误", f"计算函数值时出错:{str(e)}")
            return
        ax.plot(x, y, "b-", linewidth=2.5)
        ax.set_title(title, fontsize=16, pad=20)
        ax.set_xlabel("x", fontsize=12, labelpad=-10, x=1.02)
        ax.set_ylabel("y", fontsize=12, labelpad=-20, y=1.02, rotation=0)
        self.optimize_ticks(ax, x_range, (np.min(y), np.max(y)))
        self.fig.tight_layout()

    def plot_implicit_equation(self, eq, x_range=(-3, 3), y_range=(-3, 3), resolution=500, levels=[0], cmap="viridis", title="隐函数图像"):
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_facecolor("white")
        self.fig.set_facecolor("white")
        x = np.linspace(x_range[0], x_range[1], resolution)
        y = np.linspace(y_range[0], y_range[1], resolution)
        X, Y = np.meshgrid(x, y)
        try:
            Z = eq(X, Y)
        except Exception as e:
            messagebox.showerror("方程错误", f"计算方程值时出错:{str(e)}")
            return
        contour = ax.contour(X, Y, Z, levels=levels, colors="red", linewidths=2.5)
        if len(levels) > 1:
            ax.contourf(X, Y, Z, levels=np.linspace(Z.min(), Z.max(), 100), cmap=cmap, alpha=0.6)
        cbar = self.fig.colorbar(contour)
        cbar.set_label("F(x, y)", rotation=270, labelpad=20)
        ax.grid(True, linestyle="--", alpha=0.4)
        ax.set_aspect("equal")
        ax.set_title(title, fontsize=16, pad=20)
        ax.set_xlabel("x", fontsize=12)
        ax.set_ylabel("y", fontsize=12)
        ax.axhline(0, color="black", linewidth=0.8, alpha=0.7)
        ax.axvline(0, color="black", linewidth=0.8, alpha=0.7)
        self.optimize_ticks(ax, x_range, y_range)
        self.fig.tight_layout()

    def optimize_ticks(self, ax, x_range, y_range):
        x_min, x_max = x_range
        y_min, y_max = y_range
        x_span = x_max - x_min
        y_span = y_max - y_min
        x_major_locator = ticker.MaxNLocator(nbins=7)
        y_major_locator = ticker.MaxNLocator(nbins=7)
        ax.xaxis.set_major_locator(x_major_locator)
        ax.yaxis.set_major_locator(y_major_locator)

    def plot_heart_curve(self):
        self.fig.clear()
        ax1 = self.fig.add_subplot(111)
        ax1.set_aspect("equal")
        ax1.set_title("心形线:$(x^2+y^2-1)^3 - x^2y^3 = 0$", fontsize=14)
        ax1.set_facecolor("white")
        self.fig.set_facecolor("white")
        def heart_eq(x, y):
            return (x**2 + y**2 - 1)**3 - x**2 * y**3
        x = np.linspace(-1.5, 1.5, 500)
        y = np.linspace(-1.5, 1.5, 500)
        X, Y = np.meshgrid(x, y)
        Z = heart_eq(X, Y)
        contour = ax1.contour(X, Y, Z, levels=[0], colors="red", linewidths=3)
        ax1.contourf(X, Y, Z, levels=[-1000, 0], colors=["pink"], alpha=0.4)
        ax1.grid(True, linestyle="--", alpha=0.3)
        ax1.set_xlim(-1.5, 1.5)
        ax1.set_ylim(-1.5, 1.5)
        self.optimize_ticks(ax1, (-1.5, 1.5), (-1.5, 1.5))
        self.fig.tight_layout()
        self.canvas.draw()

    def plot_electric_potential(self):
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_facecolor("white")
        self.fig.set_facecolor("white")
        charges = [{"x": -1, "y": 0, "q": 1}, {"x": 1, "y": 0, "q": -1}]
        x = np.linspace(-2.5, 2.5, 500)
        y = np.linspace(-2, 2, 500)
        X, Y = np.meshgrid(x, y)
        V = np.zeros_like(X)
        for charge in charges:
            r = np.sqrt((X - charge["x"])**2 + (Y - charge["y"])**2)
            V += charge["q"] / r
            V = np.nan_to_num(V, posinf=10, neginf=-10)
        levels = np.linspace(-10, 10, 21)
        contourf = ax.contourf(X, Y, V, levels=levels, cmap="coolwarm", alpha=0.8)
        contour = ax.contour(X, Y, V, levels=levels, colors="k", linewidths=0.5)
        ax.clabel(contour, inline=True, fontsize=8)
        for charge in charges:
            color = "red" if charge["q"] > 0 else "blue"
            marker = "+" if charge["q"] > 0 else "_"
            ax.scatter(charge["x"], charge["y"], s=300, c=color, marker=marker, linewidths=2)
            ax.text(charge["x"], charge["y"]+0.2, f"{charge['q']}q", ha="center", fontsize=12, weight="bold")
        ax.set_title("两个点电荷的电势分布", fontsize=16, pad=20)
        ax.set_xlabel("x (m)", fontsize=12)
        ax.set_ylabel("y (m)", fontsize=12)
        ax.set_aspect("equal")
        ax.grid(True, linestyle="--", alpha=0.4)
        ax.axhline(0, color="k", linewidth=0.8, alpha=0.7)
        ax.axvline(0, color="k", linewidth=0.8, alpha=0.7)
        ax.text(1.5, 1.8, r"$V = \sum \frac{kq_i}{r_i}$", fontsize=14, bbox=dict(facecolor="white", alpha=0.8))
        cbar = self.fig.colorbar(contourf, label="电势 (V)")
        self.optimize_ticks(ax, (-2.5, 2.5), (-2, 2))
        self.fig.tight_layout()
        self.canvas.draw()

    def get_function_label(self, func_str):
        if any(word in func_str.lower() for word in ["import", "os", "sys", "subprocess"]):
            raise ValueError("检测到不安全的代码")
        safe_str = func_str
        replacements = {
            r'np\.sin\(([^)]+)\)': r'\sin(\1)',
            r'np\.cos\(([^)]+)\)': r'\cos(\1)',
            r'np\.tan\(([^)]+)\)': r'\tan(\1)',
            r'np\.exp\(([^)]+)\)': r'\exp(\1)',
            r'np\.sqrt\(([^)]+)\)': r'\sqrt{\1}',
            r'np\.log\(([^)]+)\)': r'\ln(\1)',
            r'np\.pi': r'\pi',
            r'\*\*': r'^',
            r'\*': r'\cdot',
        }
        for pattern, replacement in replacements.items():
            try:
                safe_str = re.sub(pattern, replacement, safe_str)
            except re.error:
                continue
        if '/' in safe_str:
            if re.search(r'\d+\.?\d*/\d+\.?\d*', safe_str):
                parts = safe_str.split('/')
                if len(parts) == 2:
                    numerator = parts[0].strip()
                    denominator = parts[1].strip()
                    safe_str = r'\frac{' + numerator + '}{'+ denominator +'}'
        return safe_str

    def save_image(self):
        try:
            filename = simpledialog.askstring("保存图像", "请输入文件名:", initialvalue="function_plot.png")
            if filename:
                if not filename.endswith(".png"): filename += ".png"
                self.fig.savefig(filename, dpi=150, bbox_inches="tight")
                messagebox.showinfo("成功", f"图像已保存至:{os.path.abspath(filename)}")
        except Exception as e:
            messagebox.showerror("保存错误", f"保存图像时出错:{e}")

def main():
    root = tk.Tk()
    style = ttk.Style()
    style.configure("TFrame", background="#f5f5f5")
    style.configure("TLabelframe", background="#ffffff", relief="sunken")
    style.configure("TLabelframe.Label", background="#ffffff", font=("SimHei", 10, "bold"))
    style.configure("TButton", padding=5)
    try:
        plt.rcParams["font.family"] = ["SimHei"]
    except:
        try:
            plt.rcParams["font.family"] = ["WenQuanYi Micro Hei"]
        except:
            plt.rcParams["font.family"] = ["DejaVu Sans", "sans-serif"]
            print("警告:未找到中文字体,图表文字可能无法正确显示")
    app = FunctionVisualizer(root)
    root.mainloop()

if __name__ == "__main__":
    main()

总结

该工具实现了从表达式输入到图形渲染的完整闭环。通过白名单机制保障了安全性,利用 Matplotlib 的丰富功能满足了教学与科研需求。后续可扩展极坐标绘图、导数积分可视化或 3D 场景,进一步提升其在工程仿真中的应用价值。

目录

  1. Python 数学可视化实战:显函数、隐函数与复杂曲线的交互式绘图
  2. 核心架构与安全机制
  3. 界面与计算分层
  4. 安全表达式解析
  5. 显函数可视化
  6. 核心实现逻辑
  7. 案例演示
  8. 隐函数可视化
  9. 核心实现逻辑
  10. 案例演示
  11. 特色曲线与物理应用
  12. 心形线(数学艺术)
  13. 电势分布(物理应用)
  14. 交互式 GUI 设计
  15. 界面布局
  16. 动态控件更新
  17. 完整代码实现
  18. 设置 matplotlib 支持中文显示
  19. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • C++ 面试高频考点与核心技术解析
  • 使用 Ollama 运行 HuggingFace 下载的本地模型
  • 行空板 K10 与 Mind+ 零基础 AI 入门实战
  • TCP/IP 基础概念与 C/C++ Socket 编程实战指南
  • Whisper 模型本地化部署:版本下载与离线环境搭建
  • Matlab Copilot_AI 工具箱:对接多款 AI 大模型辅助编程
  • 使用 Continue 插件本地部署 AI 代码助手替代 Cursor 或 Copilot
  • AI 数学基础:Tokenization 如何将文本转换为数字
  • Nuxt 4 生产环境部署指南 (Node.js + Nginx)
  • SPI 主控制器设计、仿真与 FPGA 验证(含 XIP 模式)
  • 2025 年主流 ChatGPT 桌面客户端对比与配置指南
  • Stable Diffusion 3.5 FP8 模型在 AIGC 平台的应用与优化
  • AI 大模型算法工程师求职指南与高薪职位攻略
  • Coze 打造专属 AI 应用:从智能体到 Web 部署指南
  • MCP 插件配置指南:以 browser-tools-mcp 为例
  • Node.js 版本管理指南:卸载、安装 NVM 及常见问题解决
  • Vue3 Composition API 方法调用失效排查:script setup 暴露机制解析
  • AI Coding 入门指南:工具选择与实战技巧
  • HarmonyOS6 RcButton 组件样式系统深度剖析
  • 基于 Claude Code + GLM-4.7 的前端生成与评测实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online