那天我刚想下班摸鱼,办公室门一开,班主任冲进来就问:“东哥,你会不会搞那个……Python?帮我弄个成绩分析,我这Excel都要被我点炸了。” 我看了一眼她那张一万多行的表,脑袋“嗡”了一下:得,经典背锅现场这不就来了嘛。
一边给她倒水,我一边琢磨:要不就整一个简单的桌面程序,点几下按钮就能算平均分、查挂科名单、看年级前十那种。对老师来说,打开个浏览器都觉得复杂,还是老老实实用 Tkinter 弹个窗口,数据用 SQLite3 丢在本地一个文件里,拷U盘就能带走,简单方便。
说干就干。最基础的就是建库,我随手写了个初始化的函数,虽然看着糙,但管用:
import sqlite3
DB_PATH = "score.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
math REAL NOT NULL,
chinese REAL NOT NULL,
english REAL NOT NULL
)
""")
conn.commit()
conn.close()
这玩意儿的好处是啥?就算老师手一哆嗦把 score.db 给删了,下次再打开程序,它自己又能给你建一张新表。顶多就是数据没了,但程序照常跑,属于那种“人没了系统还在”的顽强。
接下来就是最日常的需求:录入一个学生成绩。老师的要求特别直白:“我就想输名字、输分数,点一下按钮,完事。” 其他什么CRUD、API这些词,就别跟她讲了。行,那就按她的意思来:
def add_student(name, math, chinese, english):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"INSERT INTO students(name, math, chinese, english) VALUES (?, ?, ?, ?)",
(name, math, chinese, english)
)
conn.commit()
conn.close()
别看就这么几行,后来还真救了我一次。有天老师很激动地跟我说:“我刚才不小心点了两次添加怎么办?” 我心里咯噔一下,寻思是不是得加个撤销功能。冷静下来翻了翻数据库,好家伙,她第二次输的时候把数学分从95改成了96,这不就是“人工纠错”嘛。我赶紧顺势说:“这说明咱们系统有智能纠偏能力。”——这叫算法辅助教学,懂吧?
光能存还不够,老师最关心的是谁考砸了,谁需要叫家长。这个功能写起来就有点“阴间”了,一边写我一边庆幸自己毕业多年。先得有个基础统计,算算平均分、最高分这些:
def calc_stats():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
SELECT
COUNT(*) as total,
AVG(math), AVG(chinese), AVG(english),
MAX(math), MAX(chinese), MAX(english)
FROM students
""")
row = cur.fetchone()
conn.close()
return {
"total": row[0],
"avg_math": row[1],
"avg_chinese": row[2],
"avg_english": row[3],
"max_math": row[4],
"max_chinese": row[5],
"max_english": row[6],
}
写完我自己先跑了一遍,看到 AVG 函数算出来小数点后那一大串数字,脑子里立刻浮现出老师拿着计算器对着屏幕一位位核对的样子。得,后面做UI的时候,我干脆都把数字四舍五入到一位小数,不给她留下任何“质疑程序精度”的机会。
说了半天数据库,得回到Tkinter这边了。老师的指示非常明确:“要能点,能看,别让我记命令。” 没辙,给她糊一个简单粗暴的窗口吧。当时大概是这么搭的框架:
import tkinter as tk
from tkinter import messagebox
def build_ui():
root = tk.Tk()
root.title("学生成绩分析系统")
tk.Label(root, text="姓名").grid(row=0, column=0)
tk.Label(root, text="数学").grid(row=1, column=0)
tk.Label(root, text="语文").grid(row=2, column=0)
tk.Label(root, text="英语").grid(row=3, column=0)
name_var = tk.StringVar()
math_var = tk.StringVar()
chi_var = tk.StringVar()
eng_var = tk.StringVar()
tk.Entry(root, textvariable=name_var).grid(row=0, column=1)
tk.Entry(root, textvariable=math_var).grid(row=1, column=1)
tk.Entry(root, textvariable=chi_var).grid(row=2, column=1)
tk.Entry(root, textvariable=eng_var).grid(row=3, column=1)
result_box = tk.Text(root, width=40, height=10)
result_box.grid(row=0, column=2, rowspan=6, padx=10)
def on_add():
name = name_var.get().strip()
try:
math = float(math_var.get())
chi = float(chi_var.get())
eng = float(eng_var.get())
except ValueError:
messagebox.showerror("错误", "分数必须是数字,别夹中文逗号了…")
return
if not name:
messagebox.showwarning("提示", "姓名不能为空")
return
add_student(name, math, chi, eng)
messagebox.showinfo("成功", "已保存")
name_var.set("")
math_var.set("")
chi_var.set("")
eng_var.set("")
def on_stats():
data = calc_stats()
result_box.delete("1.0", tk.END)
if data["total"] == 0:
result_box.insert(tk.END, "还没有任何学生数据\n")
return
result_box.insert(tk.END, f"总人数:{data['total']}\n")
result_box.insert(tk.END, f"数学 平均:{data['avg_math']:.1f} 最高:{data['max_math']}\n")
result_box.insert(tk.END, f"语文 平均:{data['avg_chinese']:.1f} 最高:{data['max_chinese']}\n")
result_box.insert(tk.END, f"英语 平均:{data['avg_english']:.1f} 最高:{data['max_english']}\n")
tk.Button(root, text="添加成绩", command=on_add).grid(row=4, column=1, sticky="ew", pady=5)
tk.Button(root, text="统计分析", command=on_stats).grid(row=5, column=1, sticky="ew", pady=5)
return root
中间那个 result_box 文本框特别重要。一开始我偷懒,所有结果都用 messagebox 弹窗显示,老师用了一会儿就跟我说:“你这玩意儿弹得我想砸电脑。” 我赶紧改成右边放一个大文本框,所有统计信息都往里面刷新,视觉上瞬间就“温柔”多了。
你以为这就结束了?并没有。真正的大坑在“挂科名单”这里。老师轻描淡写地补了一句:“最好能看看谁挂了两门以上。” 好嘛,一听就知道得写带条件的SQL。我一边心里碎碎念,一边加了个函数,写法比较直接:
def get_failed_students(threshold=60):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
SELECT name, math, chinese, english
FROM students
""")
res = []
for name, m, c, e in cur.fetchall():
fail_count = sum(score < threshold for score in (m, c, e))
if fail_count >= 2:
res.append((name, m, c, e, fail_count))
conn.close()
return res
然后在UI里,我偷偷加了个对应的按钮和事件:
def on_failed():
rows = get_failed_students()
result_box.delete("1.0", tk.END)
if not rows:
result_box.insert(tk.END, "没有两门以上不及格的学生,老师今天可以开心回家了。\n")
return
for name, m, c, e, cnt in rows:
line = f"{name} 数学:{m} 语文:{c} 英语:{e} 挂科数:{cnt}\n"
result_box.insert(tk.END, line)
tk.Button(root, text="挂科名单", command=on_failed).grid(row=6, column=1, sticky="ew", pady=5)
那天我把新版本拷到老师电脑上,她点开“挂科名单”按钮,盯着屏幕沉默了大概三秒钟,然后回头跟我说:“这个按钮,你别让学生看到。” 我秒懂:得,这属于内部运维工具,不对普通用户开放。
最后,得把这些零散的模块串起来,不然总有人复制了代码少一块,回头又来问“为什么点按钮没反应”。主函数就很简单:
if __name__ == "__main__":
init_db()
app = build_ui()
app.mainloop()
整套系统没啥高大上的,就是一个基于 Tkinter 和 SQLite3 的本地小工具,但胜在足够接地气:老师双击一个图标,就能录入成绩、看平均分、查挂科名单;数据库就一个 score.db 文件,U盘拷来拷去也不怕环境问题;哪天要是真想弄到服务器上,把 SQLite3 换成 MySQL 或别的,也无非是改掉那几个 sqlite3.connect 的地方,核心逻辑基本不用动。
做完这个东西之后,老师每次见我都笑眯眯的。唯一的小“后遗症”就是,她现在看到我就说:“东哥,下次再给我加个导出Excel的功能哈。” 行吧,下次的事下次再说,说不定下次的需求就变成“学生成绩可视化大屏系统”了……
这个小项目虽然简单,但很好地展示了如何用 Python 快速解决身边的具体问题。如果你也对这类实用的桌面开发或数据处理感兴趣,欢迎来 云栈社区 一起交流探讨,这里有很多类似的实战项目和经验分享。