Python 数学可视化:显函数、隐函数及复杂曲线交互绘图
介绍基于 Python Tkinter 和 Matplotlib 库开发的函数与方程可视化工具。支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图。核心功能包括安全的表达式解析(白名单机制防止注入)、动态 GUI 界面、图像保存及 LaTeX 公式渲染。工具适用于数学教学、工程仿真及科学研究,帮助用户直观理解数学关系。

介绍基于 Python Tkinter 和 Matplotlib 库开发的函数与方程可视化工具。支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图。核心功能包括安全的表达式解析(白名单机制防止注入)、动态 GUI 界面、图像保存及 LaTeX 公式渲染。工具适用于数学教学、工程仿真及科学研究,帮助用户直观理解数学关系。

在科学计算和数据分析中,函数与方程的可视化是理解数学关系和物理现象的重要工具。本文基于 Python 的 Tkinter 和 Matplotlib 库,实现一个功能完善的函数与方程可视化工具,支持显函数、隐函数、特殊曲线(如心形线)及物理场分布(如电势)的交互式绘图,并提供安全的表达式解析、图像保存等功能。
np.linspace 生成采样点,安全计算函数值contour 绘制等值线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
def safe_eval(expr, namespace):
"""安全执行表达式"""
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
}
safe_globals = {"__builtins__": None}
safe_locals = {**allowed_funcs, **namespace}
compiled_code = compile(expr, '<string>', 'eval')
return eval(compiled_code, safe_globals, safe_locals)
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()))
# 预设函数定义
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$",
}
}
![图片]
plot_explicit("1/x", x_range=(-5, 5)) # 输入表达式直接绘制
![图片]
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)')
# 预设隐函数
self.implicit_presets["圆"] = {
"eq": lambda x, y: x**2 + y**2 - 4,
"title": "圆:$x^2 + y^2 = 4$",
}
![图片]
plot_implicit("x**3 + y**3 - 3*x*y", x_range=(-3, 3), y_range=(-3, 3))
![图片]
def plot_heart_curve(self):
"""笛卡尔心形线"""
eq = lambda x, y: (x**2 + y**2 - 1)**3 - x**2 * y**3
self.plot_implicit_equation(eq, x_range=(-1.5, 1.5), y_range=(-1.5, 1.5))
self.fig.contourf(..., colors='pink', alpha=0.4) # 填充爱心区域
![图片]
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') # 温度映射显示电势
self.fig.scatter([c['x']], [c['y']], s=300, c=['red', 'blue'], marker='+')
![图片]
def __init__(self, root):
self.root = root
self.root.geometry("1200x800")
# 左侧控制面板
left_frame = ttk.LabelFrame(root, text="可视化选项")
ttk.Radiobutton(left_frame, text="显函数", variable=self.viz_type, value="explicit")
ttk.Radiobutton(left_frame, text="隐函数", variable=self.viz_type, value="implicit")
ttk.Radiobutton(left_frame, text="心形线", variable=self.viz_type, value="heart")
# 右侧绘图区域
self.canvas = FigureCanvasTkAgg(self.fig, master=right_frame)
self.toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)
# 集成缩放工具
def update_controls(self):
"""根据选择类型显示对应控件"""
if self.viz_type.get() == "explicit":
self.explicit_frame.pack()
self.update_preset_options(self.explicit_presets.keys())
elif self.viz_type.get() == "implicit":
self.implicit_frame.pack()
self.update_preset_options(self.implicit_presets.keys())
# 隐藏其他面板
def save_image(self):
filename = simpledialog.askstring("保存", "文件名")
if filename:
self.fig.savefig(f"{filename}.png", dpi=150, bbox_inches="tight")
messagebox.showinfo("成功", f"保存至:{os.path.abspath(filename)}")
def get_function_label(self, expr):
"""生成 LaTeX 公式"""
expr = expr.replace('np.sin', '\\sin').replace('**', '^')
expr = re.sub(r'(\d)/(\d)', r'\\frac{\1}{\2}', expr) # 自动转换分数
return f"${expr}$"
本文实现的函数可视化工具具备以下特点:
该工具可广泛应用于数学教学、工程仿真、科学研究等领域,帮助用户快速建立数学表达式与图形之间的直观联系。
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 matplotlib.patches as patches
from matplotlib import ticker
from matplotlib.colors import ListedColormap
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.bg_color = "#f5f5f5"
self.frame_color = "#ffffff"
self.button_color =
.button_text_color =
.explicit_presets = {
: {
: x: x** - *x,
: ,
: (-, ),
: ,
},
: {
: x: /x,
: ,
: (-, ),
: ,
},
: {
: x: np.exp(x),
: ,
: (-, ),
: ,
},
}
.implicit_presets = {
: {
: x, y: x** + y** - ,
: ,
: (-, ),
: (-, ),
: ,
},
: {
: x, y: x**/ + y**/ - ,
: ,
: (-, ),
: (-, ),
: ,
},
: {
: x, y: x** - y** - ,
: ,
: (-, ),
: (-, ),
: ,
},
: {
: x, y: x** + y** - *x*y,
: ,
: (-, ),
: (-, ),
: ,
},
}
main_frame = ttk.Frame(.root, padding=)
main_frame.pack(fill=tk.BOTH, expand=)
left_frame = ttk.LabelFrame(main_frame, text=, padding=, width=)
left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(, ))
left_frame.pack_propagate()
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=)
.plot_frame = ttk.Frame(right_frame)
.plot_frame.pack(fill=tk.BOTH, expand=, padx=, pady=)
.fig = Figure(figsize=(, ), dpi=)
.canvas = FigureCanvasTkAgg(.fig, master=.plot_frame)
.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=)
.toolbar_frame = ttk.Frame(right_frame, height=)
.toolbar_frame.pack(fill=tk.X, padx=, pady=(, ))
.toolbar = NavigationToolbar2Tk(.canvas, .toolbar_frame)
.toolbar.update()
.create_controls(left_frame)
.plot_predefined_function()
():
ttk.Label(parent, text=, font=(, , )).pack(anchor=tk.W, pady=(, ))
.viz_type = tk.StringVar(value=)
types = [(, ), (, ), (, ), (, )]
text, value types:
ttk.Radiobutton(parent, text=text, variable=.viz_type, value=value, command=.update_controls).pack(anchor=tk.W, padx=, pady=)
.preset_frame = ttk.LabelFrame(parent, text=, padding=)
.preset_frame.pack(fill=tk.X, pady=)
.preset_functions = tk.StringVar()
.preset_combobox = ttk.Combobox(.preset_frame, textvariable=.preset_functions, width=)
.preset_combobox.pack(fill=tk.X, pady=)
ttk.Button(.preset_frame, text=, command=.plot_predefined_function).pack(fill=tk.X, pady=)
.explicit_frame = ttk.LabelFrame(parent, text=, padding=)
.explicit_frame.pack(fill=tk.X, pady=)
ttk.Label(.explicit_frame, text=).pack(anchor=tk.W)
.explicit_entry = ttk.Entry(.explicit_frame, width=)
.explicit_entry.insert(, )
.explicit_entry.pack(fill=tk.X, pady=)
ttk.Label(.explicit_frame, text=).pack(anchor=tk.W)
.x_range_entry = ttk.Entry(.explicit_frame, width=)
.x_range_entry.insert(, )
.x_range_entry.pack(fill=tk.X, pady=)
ttk.Button(.explicit_frame, text=, command=.plot_explicit).pack(fill=tk.X, pady=)
.implicit_frame = ttk.LabelFrame(parent, text=, padding=)
.implicit_frame.pack(fill=tk.X, pady=)
ttk.Label(.implicit_frame, text=).pack(anchor=tk.W)
.implicit_entry = ttk.Entry(.implicit_frame, width=)
.implicit_entry.insert(, )
.implicit_entry.pack(fill=tk.X, pady=)
ttk.Label(.implicit_frame, text=).pack(anchor=tk.W)
.implicit_x_range_entry = ttk.Entry(.implicit_frame, width=)
.implicit_x_range_entry.insert(, )
.implicit_x_range_entry.pack(fill=tk.X, pady=)
ttk.Label(.implicit_frame, text=).pack(anchor=tk.W)
.implicit_y_range_entry = ttk.Entry(.implicit_frame, width=)
.implicit_y_range_entry.insert(, )
.implicit_y_range_entry.pack(fill=tk.X, pady=)
ttk.Button(.implicit_frame, text=, command=.plot_implicit).pack(fill=tk.X, pady=)
ttk.Button(parent, text=, command=.save_image).pack(side=tk.BOTTOM, pady=)
.update_controls()
():
viz_type = .viz_type.get()
.preset_frame.pack_forget()
.explicit_frame.pack_forget()
.implicit_frame.pack_forget()
viz_type == :
.explicit_frame.pack(fill=tk.X, pady=)
.update_preset_options(.explicit_presets.keys())
viz_type == :
.implicit_frame.pack(fill=tk.X, pady=)
.update_preset_options(.implicit_presets.keys())
viz_type == :
.plot_heart_curve()
viz_type == :
.plot_electric_potential()
.preset_frame.pack(fill=tk.X, pady=)
():
options :
options = []
.preset_combobox[] = (options)
options:
.preset_functions.((options)[])
():
viz_type = .viz_type.get()
selected = .preset_functions.get()
.fig.clear()
ax = .fig.add_subplot()
ax.set_facecolor()
.fig.set_facecolor()
viz_type == selected .explicit_presets:
data = .explicit_presets[selected]
.plot_explicit_function(f=data[], x_range=data[], title=data[])
.explicit_entry.delete(, tk.END)
.explicit_entry.insert(, data[])
.x_range_entry.delete(, tk.END)
.x_range_entry.insert(, )
viz_type == selected .implicit_presets:
data = .implicit_presets[selected]
.plot_implicit_equation(eq=data[], x_range=data[], y_range=data[], title=data[])
.implicit_entry.delete(, tk.END)
.implicit_entry.insert(, data[])
.implicit_x_range_entry.delete(, tk.END)
.implicit_x_range_entry.insert(, )
.implicit_y_range_entry.delete(, tk.END)
.implicit_y_range_entry.insert(, )
.canvas.draw()
():
allowed_chars = ()
cleaned = expr.replace(, ).replace(, )
invalid_chars = (cleaned) - allowed_chars
invalid_chars:
ValueError()
stack = []
char expr:
char == :
stack.append(char)
char == :
stack:
ValueError()
stack.pop()
stack:
ValueError()
():
:
.is_valid_expression(expr)
expr = expr.replace(, )
allowed_funcs = {
: np,
: np.sin,
: np.cos,
: np.tan,
: np.exp,
: np.sqrt,
: np.log,
: np.pi,
: np.arctan2,
}
safe_globals = {: }
safe_locals = {**allowed_funcs, **namespace}
compiled_code = (expr, , )
(compiled_code, safe_globals, safe_locals)
Exception e:
ValueError()
():
:
func_str = .explicit_entry.get().strip()
x_range_str = .x_range_entry.get().strip()
func_str x_range_str:
ValueError()
x_min, x_max = (, x_range_str.split())
x_min >= x_max:
ValueError()
x_vals = np.linspace(x_min, x_max, )
y_vals = np.zeros_like(x_vals)
i, x (x_vals):
y_vals[i] = .safe_eval(func_str, {: x})
.plot_explicit_function(f= x: y_vals, x_range=(x_min, x_max), title=)
.canvas.draw()
Exception e:
messagebox.showerror(, )
():
:
eq_str = .implicit_entry.get().strip()
x_range_str = .implicit_x_range_entry.get().strip()
y_range_str = .implicit_y_range_entry.get().strip()
eq_str x_range_str y_range_str:
ValueError()
x_min, x_max = (, x_range_str.split())
y_min, y_max = (, y_range_str.split())
x_min >= x_max y_min >= y_max:
ValueError()
eq = X, Y: .safe_eval(eq_str, {: X, : Y})
.plot_implicit_equation(eq=eq, x_range=(x_min, x_max), y_range=(y_min, y_max), title=)
.canvas.draw()
Exception e:
messagebox.showerror(, )
():
.fig.clear()
ax = .fig.add_subplot()
ax.set_facecolor()
.fig.set_facecolor()
ax.grid(, linestyle=, alpha=)
ax.spines[].set_position()
ax.spines[].set_position()
ax.spines[].set_visible()
ax.spines[].set_visible()
x = np.linspace(x_range[], x_range[], )
:
y = f(x)
Exception e:
messagebox.showerror(, )
ax.plot(x, y, , linewidth=)
ax.set_title(title, fontsize=, pad=)
ax.set_xlabel(, fontsize=, labelpad=-, x=)
ax.set_ylabel(, fontsize=, labelpad=-, y=, rotation=)
.optimize_ticks(ax, x_range, (np.(y), np.(y)))
.fig.tight_layout()
():
.fig.clear()
ax = .fig.add_subplot()
ax.set_facecolor()
.fig.set_facecolor()
x = np.linspace(x_range[], x_range[], resolution)
y = np.linspace(y_range[], y_range[], resolution)
X, Y = np.meshgrid(x, y)
:
Z = eq(X, Y)
Exception e:
messagebox.showerror(, )
contour = ax.contour(X, Y, Z, levels=levels, colors=, linewidths=)
(levels) > :
ax.contourf(X, Y, Z, levels=np.linspace(Z.(), Z.(), ), cmap=cmap, alpha=)
cbar = .fig.colorbar(contour)
cbar.set_label(, rotation=, labelpad=)
ax.grid(, linestyle=, alpha=)
ax.set_aspect()
ax.set_title(title, fontsize=, pad=)
ax.set_xlabel(, fontsize=)
ax.set_ylabel(, fontsize=)
ax.axhline(, color=, linewidth=, alpha=)
ax.axvline(, color=, linewidth=, alpha=)
.optimize_ticks(ax, x_range, y_range)
.fig.tight_layout()
():
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=)
y_major_locator = ticker.MaxNLocator(nbins=)
ax.xaxis.set_major_locator(x_major_locator)
ax.yaxis.set_major_locator(y_major_locator)
():
.fig.clear()
ax1 = .fig.add_subplot()
ax1.set_aspect()
ax1.set_title(, fontsize=)
ax1.set_facecolor()
.fig.set_facecolor()
():
(x** + y** - )** - x** * y**
x = np.linspace(-, , )
y = np.linspace(-, , )
X, Y = np.meshgrid(x, y)
Z = heart_eq(X, Y)
contour = ax1.contour(X, Y, Z, levels=[], colors=, linewidths=)
ax1.contourf(X, Y, Z, levels=[-, ], colors=[], alpha=)
ax1.grid(, linestyle=, alpha=)
ax1.set_xlim(-, )
ax1.set_ylim(-, )
.optimize_ticks(ax1, (-, ), (-, ))
.fig.tight_layout()
.canvas.draw()
():
.fig.clear()
ax = .fig.add_subplot()
ax.set_facecolor()
.fig.set_facecolor()
charges = [
{: -, : , : },
{: , : , : -},
]
x = np.linspace(-, , )
y = np.linspace(-, , )
X, Y = np.meshgrid(x, y)
V = np.zeros_like(X)
charge charges:
r = np.sqrt((X - charge[])** + (Y - charge[])**)
V += charge[] / r
V = np.nan_to_num(V, posinf=, neginf=-)
levels = np.linspace(-, , )
contourf = ax.contourf(X, Y, V, levels=levels, cmap=, alpha=)
contour = ax.contour(X, Y, V, levels=levels, colors=, linewidths=)
ax.clabel(contour, inline=, fontsize=)
charge charges:
color = charge[] >
marker = charge[] >
ax.scatter(charge[], charge[], s=, c=color, marker=marker, linewidths=)
ax.text(charge[], charge[]+, , ha=, fontsize=, weight=)
ax.set_title(, fontsize=, pad=)
ax.set_xlabel(, fontsize=)
ax.set_ylabel(, fontsize=)
ax.set_aspect()
ax.grid(, linestyle=, alpha=)
ax.axhline(, color=, linewidth=, alpha=)
ax.axvline(, color=, linewidth=, alpha=)
ax.text(, , , fontsize=, bbox=(facecolor=, alpha=))
cbar = .fig.colorbar(contourf, label=)
.optimize_ticks(ax, (-, ), (-, ))
.fig.tight_layout()
.canvas.draw()
():
(word func_str.lower() word [, , , ]):
ValueError()
safe_str = func_str
replacements = {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
}
pattern, replacement replacements.items():
:
safe_str = re.sub(pattern, replacement, safe_str)
re.error e:
safe_str:
re.search(, safe_str):
parts = safe_str.split()
(parts) == :
numerator = parts[].strip()
denominator = parts[].strip()
safe_str = + numerator + + + denominator +
safe_str
():
:
filename = simpledialog.askstring(, , initialvalue=)
filename:
filename.endswith():
filename +=
.fig.savefig(filename, dpi=, bbox_inches=)
messagebox.showinfo(, )
Exception e:
messagebox.showerror(, )
():
root = tk.Tk()
style = ttk.Style()
style.configure(, background=)
style.configure(, background=, relief=)
style.configure(, background=, font=(, , ))
style.configure(, padding=)
:
plt.rcParams[] = []
:
:
plt.rcParams[] = []
:
:
plt.rcParams[] = []
:
:
plt.rcParams[] = []
:
plt.rcParams[] = [, ]
()
app = FunctionVisualizer(root)
root.mainloop()
__name__ == :
main()

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online