10-拓展应用
约 7696 字大约 26 分钟
2026-10-31
走过前九章,这条主线已经完整:从变量与分支循环起步,学会用字符串、列表、字典组织数据,再用函数封装逻辑、用类聚合数据,最后把成绩数据依次交给了模块、文件和数据库。必修的地基已经打完,但Python的世界远不止这些——它能画图、能弹窗口、能和其他程序交换数据,还藏着不少让代码更简洁、更专业的写法。
本章是一张自助菜单:五节内容各自独立、互不依赖,可以按兴趣任选,也可以全部跳过而不影响主线。我们会用海龟库画出一辆彩色风车,用一行代码干掉五行的循环,用json把成绩单存成通用的交换格式,用tkinter给例题8-3装上图形界面,最后了解两个"进阶写法"——自定义异常与装饰器。贯穿成绩管理这条老主线的宗旨不变,只是这一次,它会以更炫的样子登场。
需要说明的是:本章各节内容均标注为选学,学习目标也相应改为"了解、体验"级别,不作为考核要求。学有余力的同学可以由此推开Python世界更宽的一扇门。
【学习目标】
知识目标
了解turtle海龟库的基本绘图原理,认识前进、转向、抬笔、落笔等常用指令。
理解列表推导式、字典推导式的语法结构,能与for循环写法相互转换。
了解json格式的特点,认识json模块中dump/load等常用函数。
了解tkinter图形界面程序的基本结构,认识窗口、控件与事件的概念。
了解自定义异常类的定义方法,初步认识装饰器的作用。
能力目标
能够体验使用turtle绘制简单图形,尝试用循环画出正多边形与风车图案。
能够把简单的for循环改写为列表推导式或字典推导式。
能够使用json模块将字典数据保存到文件并完整读回。
能够体验编写一个带输入框和按钮的小窗口程序。
素养目标
保持对编程世界的好奇心,体会"同样的业务、不同的技术"带来的乐趣。
养成"简单逻辑用简洁写法、复杂逻辑用常规写法"的代码品味。
1.趣味绘图:turtle海龟库
1.1 初识小海龟
turtle(海龟)是Python自带的一个绘图库,它的设计思想来自上世纪六十年代的一种教学机器人:一个小机器人趴在纸上,肚子上插着画笔,你发指令"前进100步""左转90度",它就走一段、转个弯,身后留下一条线。 turtle库把这套思想搬到了屏幕上:一块画布,一只小海龟,一支笔。我们指挥海龟爬行,它就把轨迹画出来。
一支会走路的画笔——这就是海龟。你负责发号施令,它负责爬行留痕。
先来看最简单的程序:让海龟画一个边长100的正方形。
代码 10-1:用海龟画正方形
import turtle
turtle.forward(100) # 前进100像素
turtle.left(90) # 左转90度
turtle.forward(100)
turtle.left(90)
turtle.forward(100)
turtle.left(90)
turtle.forward(100)
turtle.left(90)
turtle.done() # 保持画布窗口,等用户关闭运行程序,屏幕上弹出一个窗口,一支箭头形状的"海龟"爬了四段路,留下一个正方形。四段代码几乎一模一样——只有"前进、左转"两个动作不断重复,这正是第2章for循环最擅长的事。不过作为第一次见面,先把它拆开写,看得更清楚。最后一句turtle.done()让画布在画完后保持打开状态,没有它,窗口会一闪而过。
常用指令见表10-1,本节用到哪条讲哪条。
表 10-1 海龟库常用指令一览
| 指令 | 作用 | 说明 |
|---|---|---|
| forward(d) / fd(d) | 前进d像素 | 沿当前朝向移动 |
| backward(d) / bk(d) | 后退d像素 | 不改变朝向 |
| left(a) / right(a) | 左转 / 右转a度 | 只转向,不移动 |
| penup() / pendown() | 抬笔 / 落笔 | 抬笔后移动不留痕 |
| goto(x, y) | 去到坐标(x, y) | 原点在画布正中央 |
| home() | 回到原点 | 朝向也重置为向右 |
| pencolor(c) | 设置画笔颜色 | 如"red"、"blue" |
| pensize(w) | 设置笔迹粗细 | 单位为像素 |
| speed(n) | 设置爬行速度 | 1最慢,10快,0最快 |
| begin_fill() / end_fill() | 开始 / 结束填充 | 配合fillcolor()使用 |
| done() | 保持画布 | 程序最后一句 |
1.2 画笔控制
默认状态下,海龟的笔是落下的、黑色的、粗细为1像素。想让图案更好看,就要学会"管理这支笔":换颜色、换粗细,以及在不想留痕的地方把笔抬起来。
代码 10-2:画一条虚线
import turtle
turtle.pensize(3)
turtle.pencolor("blue")
# 画五段短线,段与段之间断开
for i in range(5):
turtle.forward(30) # 落笔状态:画30像素
turtle.penup() # 抬笔
turtle.forward(20) # 抬笔状态:空走20像素
turtle.pendown() # 落笔,准备画下一段
turtle.done()虚线的原理就是"落笔画一段、抬笔空走一段"的重复。penup()和pendown()这对搭档控制着"留痕"与"不留痕",是海龟绘图里出镜率极高的组合。
再看定位。goto(x, y)让海龟直接"瞬移"到指定坐标——画布中心是原点(0, 0),向右为x轴正方向,向上为y轴正方向,和数学课上的平面直角坐标系完全一致。配合home()归位指令,可以在画布任意位置作画。
代码 10-3:在指定位置画图
import turtle
# 抬笔移动到左下角起点,避免留下空走痕迹
turtle.penup()
turtle.goto(-50, -50)
turtle.pendown()
# 画居中的大正方形
for i in range(4):
turtle.forward(100)
turtle.left(90)
# 抬笔移动到右上方,再画一个小正方形
turtle.penup()
turtle.goto(60, 60)
turtle.pendown()
for i in range(4):
turtle.forward(40)
turtle.left(90)
turtle.done()小正方形就画在大正方形的右上角外侧,两幅图互不相连——两处移动都发生在抬笔状态,没有留下痕迹。这个小技巧记住即可:先penup()再goto(),到位后再pendown()开始画。
1.3 用循环画图
代码10-1里的四段重复已经暴露了规律,现在正式用循环接管。画正多边形有一个通用公式:画n条等长的边,每画完一条就左转360÷n度。
代码 10-4:用循环画正多边形
import turtle
n = int(input("要画正几边形?(3~10):"))
length = 100
angle = 360 / n # 外角 = 360度除以边数
for i in range(n):
turtle.forward(length)
turtle.left(angle)
turtle.done()输入3就是等边三角形,输入4就是正方形,输入8就是正八边形——一个循环通吃。边数越多,图形越"鼓",当n大到一定程度,多边形就几乎变成了圆。turtle库也提供了现成的画圆指令circle(半径),它的原理其实就是画一个"边数极多的正多边形"。
最后是填色。begin_fill()和end_fill()像一对括号,把要填充的封闭路径夹在中间,fillcolor()指定填充色。
代码 10-5:画一颗填色的五角星
import turtle
turtle.fillcolor("yellow")
turtle.begin_fill()
for i in range(5):
turtle.forward(150)
turtle.left(144) # 五角星的转角是144度
turtle.end_fill()
turtle.done()运行后,一颗金黄的五角星出现在画布上。144这个角度可以自己推一推:五角星的每个尖角是36度,海龟走完一条边后转的外角是180-36=144度。
【例题 10-1】绘制彩色风车
题目:画一辆由四个正方形组成的"风车"——四个同样大小的正方形共用一个顶点,每隔90度转一个方向,分别填充红、橙、蓝、绿四种颜色,形成风车叶片的效果。
分析:单个正方形是"画四条边、每次左转90度";要画四个不同方向的正方形,只需在外层再套一个循环——每画完一个正方形,把海龟左转90度换方向,同时换一种颜色。这是典型的"循环套循环",第2章的知识在这里重逢。
代码 10-6:彩色风车
import turtle
colors = ["red", "orange", "blue", "green"] # 四种叶片颜色
side = 120 # 叶片边长
turtle.speed(0) # 0最快
for c in colors:
turtle.pencolor(c)
turtle.fillcolor(c)
turtle.begin_fill()
for i in range(4): # 画一个正方形叶片
turtle.forward(side)
turtle.left(90)
turtle.end_fill()
turtle.left(90) # 换一个方向画下一个叶片
turtle.hideturtle() # 画完把海龟藏起来
turtle.done()程序运行后,画布上出现一辆四色风车:四个正方形绕着中心点依次旋转90度,相邻叶片颜色不同,配合填充色,效果相当惊艳。这个例题的精髓是"外层换方向、内层画形状"的双层循环结构——和第2章九九乘法表"外层换行、内层打印列"如出一辙,只不过这一次,打印出来的不再是字符,而是图案。
2.简洁之道:推导式
2.1 列表推导式
第3章学过一个经典套路:遍历列表、处理每个元素、存进新列表。比如把一组成绩都加上5分的平时分:
代码 10-7:熟悉的for循环写法
scores = [85, 92, 78, 96, 60]
new_scores = [] # 先准备一个空列表
for s in scores:
new_scores.append(s + 5) # 逐个处理、逐个追加
print(new_scores) # 输出:[90, 97, 83, 101, 65]三行核心代码,做了三件事:建空列表、循环、追加。Python提供了一种把这三件事压缩成一行的写法,叫列表推导式(List Comprehension):
代码 10-8:用列表推导式改写
scores = [85, 92, 78, 96, 60]
new_scores = [s + 5 for s in scores]
print(new_scores) # 输出:[90, 97, 83, 101, 65]一行搞定,结果完全相同。怎么读这行代码?先看for部分:for s in scores——还是"遍历scores,每次取出一个s";再看for前面的部分:s + 5——"对每个s要做什么"。整体读作:对scores里的每个s,把s+5放进新列表。
把语法结构抽象出来就是:
新列表 = [表达式 for 变量 in 原序列]
对比着看更清楚,见表10-2。
表 10-2 for循环与列表推导式对照
| 场景 | for循环写法 | 推导式写法 |
|---|---|---|
| 每个元素加5 | new = [] for s in scores: new.append(s + 5) | new = [s + 5 for s in scores] |
| 挑出及格成绩 | passed = [] for s in scores: if s >= 60: passed.append(s) | passed = [s for s in scores if s >= 60] |
| 生成1~5的平方 | squares = [] for x in range(1, 6): squares.append(x * x) | squares = [x * x for x in range(1, 6)] |
可以看到,for循环版本里的append()调用、循环变量、被遍历的序列,在推导式里一个不少,只是位置重新排列了——表达式提前,for和if跟在后面。推导式不是新逻辑,只是老逻辑的紧凑写法。
2.2 带条件的推导式
推导式里的if有两种身份,位置不同、含义不同,这是最容易混淆的地方,务必分清。
第一种:if在for后面——起"过滤"作用,只留下满足条件的元素,不合格的直接淘汰:
代码 10-9:过滤条件的推导式
scores = [85, 92, 78, 96, 60, 45, 58]
passed = [s for s in scores if s >= 60]
print(passed) # 输出:[85, 92, 78, 96, 60]
# 再来一个:挑出所有姓张的名字
names = ["张三", "李四", "张伟", "王五", "张敏"]
zhangs = [n for n in names if n.startswith("张")]
print(zhangs) # 输出:['张三', '张伟', '张敏']第二种:if-else在for前面——起"变换"作用,每个元素都保留,但按条件给出不同的处理结果。注意这里必须写全"if...else...",而且位置在for之前:
代码 10-10:变换条件的推导式
scores = [85, 92, 78, 96, 60, 45, 58]
# 及格的保留原样,不及格的标记为0
fixed = [s if s >= 60 else 0 for s in scores]
print(fixed) # 输出:[85, 92, 78, 96, 60, 0, 0]
# 每个成绩给出及格/不及格标记
marks = ["及格" if s >= 60 else "不及格" for s in scores]
print(marks) # 输出:['及格', '及格', '及格', '及格', '及格', '不及格', '不及格']两种写法的记忆口诀:if在后面管"留不留",if-else在前面管"怎么变"。第2章讲过的五级成绩判定,用推导式也能一行写出来(配合第5章的函数):
def get_grade(s):
if s >= 90:
return "优秀"
elif s >= 80:
return "良好"
elif s >= 70:
return "中等"
elif s >= 60:
return "及格"
else:
return "不及格"
grades = [get_grade(s) for s in [92, 85, 74, 61, 45]]
print(grades) # 输出:['优秀', '良好', '中等', '及格', '不及格']推导式里调用函数完全合法——表达式可以是任何东西。但也请记住一条工程经验:简单逻辑用推导式,复杂逻辑用for。推导式一行塞进太多逻辑,读起来反而比循环费劲,简洁的目的是让代码更好读,不是为了行数少。
2.3 字典推导式
推导式思想同样适用于字典:花括号里放"键表达式: 值表达式"。
代码 10-11:字典推导式
names = ["张三", "李四", "王五"]
scores = [92, 85, 74]
# 把两个平行列表合成"姓名: 成绩"的字典
student_dict = {names[i]: scores[i] for i in range(len(names))}
print(student_dict) # 输出:{'张三': 92, '李四': 85, '王五': 74}
# 再把成绩换算成等级,生成"姓名: 等级"
grade_dict = {name: get_grade(score) for name, score in student_dict.items()}
print(grade_dict) # 输出:{'张三': '优秀', '李四': '良好', '王五': '中等'}第一个例子用range下标把两个列表"拉链式"合拢——正是第4章讲平行列表痛点的场景,如今一行就能建好字典;第二个例子遍历字典的items(),逐项变换生成新字典。字典推导式用得比列表推导式少,认识它、看得懂别人的代码即可。
3.数据交换格式:json
3.1 认识json
第8章用"一行一条记录、逗号分隔字段"的方式保存成绩数据,这其实就是CSV思想。CSV简单,但缺点也明显:字段一多就得靠split()逐个拆,数据类型全变回了字符串,嵌套结构(比如"每个学生还带一张成绩列表")更是没法表达。
json(JavaScript Object Notation)是另一种通用的数据交换格式,被几乎所有编程语言支持,堪称程序之间交换数据的"普通话"。它长这样:
代码 10-12:json数据的样子
# 这是一段json文本(保存为文件时的内容)
{
"name": "张三",
"age": 18,
"scores": [92, 85, 78],
"is_monitor": true
}看起来眼熟吗?花括号、冒号、逗号、方括号——和第4章的字典、列表几乎一模一样。区别只有几处细节:json的字符串必须用双引号,真值写作true/false而不是True/False,空值写作null而不是None。
对Python来说,json最贴心的一点是:它和字典、列表可以互相直接转换——存的时候把字典变成json文本,读的时候把json文本变回字典,类型、嵌套结构原封不动,一行代码搞定,再也不用split()逐个字段拆。
3.2 json模块的写入与读取
json是标准库,导入即用。写入用dump(),读取用load()——这又是一对"一个负责存、一个负责取"的搭档,和第8章的write/read同款分工。
代码 10-13:json的写入与读取
import json
student = {"name": "张三", "age": 18, "scores": [92, 85, 78]}
# 写入:把字典存进json文件
with open("student.json", "w", encoding="utf-8") as f:
json.dump(student, f, ensure_ascii=False, indent=2)
# 读取:把json文件读回字典
with open("student.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(data) # 输出:{'name': '张三', 'age': 18, 'scores': [92, 85, 78]}
print(data["scores"][0]) # 输出:92
print(type(data)) # 输出:<class 'dict'>读回来的data就是一个货真价实的字典:按第4章的规矩用键访问、取嵌套列表的下标,全部畅通无阻。dump()里还有两个常用参数:indent=2让文件里的内容带缩进、便于人阅读;ensure_ascii=False则是保存中文的必踩之坑,专门说说它。
代码 10-14:ensure_ascii参数的坑
import json
student = {"name": "张三"}
# 不加ensure_ascii=False,中文会被转成\uXXXX的转义码
with open("bad.json", "w", encoding="utf-8") as f:
json.dump(student, f)
# bad.json里存的是:{"name": "\u5f20\u4e09"}
# 加上ensure_ascii=False,中文原样保存
with open("good.json", "w", encoding="utf-8") as f:
json.dump(student, f, ensure_ascii=False)
# good.json里存的是:{"name": "张三"}两种文件用load()读回来结果相同,程序不会出错——但用记事本打开前者,看到的只是一串\u5f20\u4e03之类的天书,检查数据极不方便。养成习惯:dump()时带上ensure_ascii=False,让中文以本来面目存进文件。
json模块还有两个"字符串版"函数:dumps()把字典转成json格式的字符串(不落盘),loads()把json字符串转回字典(不读盘),适合在网络传输、界面显示等不经过文件的场景使用,认识即可。
【例题 10-2】用json保存班级成绩单
题目:把第8章例题8-3中的班级成绩数据改用json保存——录入若干学生的成绩(姓名、三门课程分数),存入scores.json,再完整读回并按总分排名输出。
分析:第8章读回CSV时,每一行都要split(",")拆字段、int()转类型、手工组装字典;换用json后,读回来的直接就是"字典的列表",这些解析工作全省了。对比之下,json的优势一目了然。
代码 10-15:json版成绩单
import json
# ---------- 1. 录入成绩 ----------
students = []
n = int(input("要录入几名学生?:"))
for i in range(n):
name = input(f"第{i + 1}位学生的姓名:")
scores = []
for subject in ["语文", "数学", "英语"]:
s = int(input(f" {subject}成绩:"))
scores.append(s)
student = {"name": name, "scores": scores, "total": sum(scores)}
students.append(student)
# ---------- 2. 存入json文件 ----------
with open("scores.json", "w", encoding="utf-8") as f:
json.dump(students, f, ensure_ascii=False, indent=2)
print("成绩单已保存到scores.json")
# ---------- 3. 读回并排名 ----------
with open("scores.json", "r", encoding="utf-8") as f:
data = json.load(f) # 直接得到字典列表,无需任何解析
# 按总分从高到低排序(第5章的key用法)
data.sort(key=lambda st: st["total"], reverse=True)
print("\n=== 成绩排名 ===")
for i, st in enumerate(data):
print(f"第{i + 1}名:{st['name']},总分{st['total']}分,明细{st['scores']}")程序运行效果如下(录入3名学生):
要录入几名学生?:3
第1位学生的姓名:张三
语文成绩:92
数学成绩:85
英语成绩:78
第2位学生的姓名:李四
语文成绩:88
数学成绩:95
英语成绩:82
第3位学生的姓名:王五
语文成绩:75
数学成绩:60
英语成绩:69
成绩单已保存到scores.json
=== 成绩排名 ===
第1名:李四,总分265分,明细[88, 95, 82]
第2名:张三,总分255分,明细[92, 85, 78]
第3名:王五,总分204分,明细[75, 60, 69]用记事本打开scores.json,能看到结构清晰、中文原样的数据。对比第8章的CSV方案:保存时少了一行join()拼接,读取时省掉了整段"逐行split、逐字段int()"的解析代码,嵌套的成绩列表也天然支持。当数据结构变复杂时,json几乎是Python小项目的首选存储格式——当然,到了第9章的规模(多用户、频繁增删改查),数据库仍是终极答案。
4.图形界面:tkinter
4.1 第一个窗口
前九章的程序都跑在"黑框框"(控制台)里,靠input()和print()与用户交流。这一节认识tkinter——Python自带的图形界面库,用它写出的程序有真正的窗口、按钮和输入框,用户用鼠标点、用键盘敲,就像使用任何一款桌面软件。
tkinter同样是标准库,导入即用。先看最小窗口程序:
代码 10-16:第一个窗口
import tkinter as tk
window = tk.Tk() # 创建主窗口
window.title("我的第一个窗口") # 设置标题
window.geometry("300x180") # 设置大小:宽300高180(注意是字母x)
window.mainloop() # 进入消息循环,窗口显示并等待运行程序,弹出一个空白窗口。最后那句mainloop()值得多说两句:控制台程序是从上到下一路执行到底,而窗口程序执行到mainloop()就"停"在这里,进入待命状态——用户点什么、敲什么,就响应什么;窗口不关,程序不结束。这个"一直等着、随叫随应"的循环叫事件循环(Event Loop),是所有图形界面程序的共同心脏。
4.2 常用控件
空窗口没什么用,往上面放东西——窗口里的按钮、输入框、文字标签,统称控件(Widget)。最常用的三个见表10-3。
表 10-3 tkinter常用控件速查
| 控件 | 作用 | 常用参数 |
|---|---|---|
| Label | 显示文字或图片 | text(内容)、font(字体)、fg(文字颜色) |
| Entry | 单行输入框 | width(宽度)、show(掩码,密码框用show="*") |
| Button | 按钮 | text(按钮文字)、command(点击时执行的函数) |
代码 10-17:三个常用控件
import tkinter as tk
window = tk.Tk()
window.title("控件演示")
window.geometry("300x180")
label = tk.Label(window, text="请输入你的姓名:") # 标签:提示文字
entry = tk.Entry(window, width=20) # 输入框
button = tk.Button(window, text="打个招呼") # 按钮(暂未绑定动作)
label.pack() # 放进窗口(pack稍后讲)
entry.pack()
button.pack()
window.mainloop()创建控件的套路是统一的:控件 = tk.控件类(放在哪个窗口, 参数...)。第一个参数都是window,表示"我要长在这个窗口上";随后用pack()把它真正摆出来。运行后窗口里出现一行提示、一个输入框、一个按钮——但点按钮没有任何反应,因为它还不知道"被点了该干什么"。
4.3 布局与事件
布局指控件在窗口里怎么摆。tkinter提供两种最简单的布局器:pack()按加入顺序从上往下依次堆放(上面的例子就是它);grid()则把窗口划成表格,用row(行)和column(列)指定每个控件的位置,适合做"标签在左、输入框在右"的表单式布局。
事件指用户的操作(点击按钮、按下键盘等),程序要用代码告诉界面:"这件事发生时,执行哪个函数"。以按钮为例,command参数就是答案:
代码 10-18:给按钮绑定动作
import tkinter as tk
def say_hello(): # 点击按钮时要执行的函数
print("你好!按钮被点击了")
window = tk.Tk()
window.title("事件演示")
button = tk.Button(window, text="点我", command=say_hello)
button.pack(pady=20)
window.mainloop()运行后每点一次按钮,控制台就打印一句"你好!按钮被点击了"。注意command=say_hello写的是函数名,不带括号——它的含义是"把这个函数交给按钮保管,点击时你再去调用它",正是第5章"函数名可以当参数传递"的知识在图形界面的直接应用。如果当时写成say_hello()带括号,函数会在程序启动时立刻执行一次,按钮反而失效。
实际开发中,按钮触发后往往要读取输入框的内容、再把结果写回标签,这就用到控件的"get"(取)与"config"(改):
代码 10-19:取值与回写
import tkinter as tk
def greet():
name = entry.get() # 读取输入框内容
label.config(text="你好," + name + "!") # 修改标签文字
window = tk.Tk()
window.title("取值与回写")
window.geometry("300x160")
label = tk.Label(window, text="请在下面输入姓名")
entry = tk.Entry(window, width=20)
button = tk.Button(window, text="打招呼", command=greet)
label.pack(pady=10)
entry.pack()
button.pack(pady=10)
window.mainloop()输入"程亮"并点击按钮,窗口上的标签立即变成"你好,程亮!"。entry.get()取回的是字符串,需要计算时要像第1章那样int()或float()转换——第1章挖的坑,在图形界面同样要小心。
【例题 10-3】图形版成绩计算器
题目:把例题5-2和例题7-1都实现过的"求平均分、判等级"功能搬上窗口:三个输入框分别录入语文、数学、Python成绩,点击"计算"按钮后,在标签上显示平均分和等级;点击"清空"按钮,三个输入框清空、标签复原。
分析:功能逻辑早已写过三遍(函数版、类版),这次纯粹是"换壳"——把input/print换成Entry和Label。等级判定沿用全书的分数线:≥90优秀、≥80良好、≥70中等、≥60及格、否则不及格。
代码 10-20:图形版成绩计算器
import tkinter as tk
from tkinter import messagebox
def get_grade(avg):
if avg >= 90:
return "优秀"
elif avg >= 80:
return "良好"
elif avg >= 70:
return "中等"
elif avg >= 60:
return "及格"
else:
return "不及格"
def calc():
try:
c = float(entry_c.get())
m = float(entry_m.get())
p = float(entry_p.get())
except ValueError:
messagebox.showwarning("输入有误", "成绩必须是数字,请重新输入")
return
avg = round((c + m + p) / 3, 1)
grade = get_grade(avg)
result.config(text=f"平均分:{avg}分,等级:{grade}")
def clear():
entry_c.delete(0, tk.END)
entry_m.delete(0, tk.END)
entry_p.delete(0, tk.END)
result.config(text="等待计算……")
window = tk.Tk()
window.title("成绩计算器")
window.geometry("320x260")
tk.Label(window, text="语文:").grid(row=0, column=0, pady=5)
entry_c = tk.Entry(window, width=15)
entry_c.grid(row=0, column=1)
tk.Label(window, text="数学:").grid(row=1, column=0, pady=5)
entry_m = tk.Entry(window, width=15)
entry_m.grid(row=1, column=1)
tk.Label(window, text="Python:").grid(row=2, column=0, pady=5)
entry_p = tk.Entry(window, width=15)
entry_p.grid(row=2, column=1)
btn_calc = tk.Button(window, text="计算", width=8, command=calc)
btn_calc.grid(row=3, column=0, pady=15)
btn_clear = tk.Button(window, text="清空", width=8, command=clear)
btn_clear.grid(row=3, column=1)
result = tk.Label(window, text="等待计算……", fg="blue", font=("微软雅黑", 12))
result.grid(row=4, column=0, columnspan=2) # 跨两列居中显示
window.mainloop()程序运行效果:窗口中三行"标签+输入框"整齐排列(grid布局的功劳),下方"计算""清空"两个按钮并排,输入88、92、85后点击"计算",蓝色结果标签立即显示"平均分:88.3分,等级:良好"。
这个例题有三处细节值得品味:其一,calc()里用第8章的try-except接住float()转换失败,用户输错不再崩溃,而是弹出警告框——graphical程序更要讲防御;其二,网格布局里row、column从0数起,与第3章索引"含头"的习惯一致;其三,"求平均分+判等级"这组功能,从例题5-2的函数、例题7-1的类,到今天登上图形窗口,同一个业务换了三种"皮肤",这正说明:功能逻辑与呈现方式是分离的,逻辑写得越好,换壳就越轻松。
5.进阶技法:自定义异常与装饰器
本节的两个内容在前面章节都留过"伏笔":第8章讲异常处理时说过"自定义异常类不讲",第5章讲函数时说过"装饰器不展开"。现在兑现承诺,把这两块拼图补上——同样属于了解级别,建立印象即可。
5.1 自定义异常类
第8章用try-except接住的都是Python内置异常(ValueError、ZeroDivisionError等)。但有些错误是"业务特有"的,Python并不认识——比如"成绩超出0~100范围",对一个成绩管理系统来说这是明确的错误,可内置异常里没有一种叫"成绩异常"。这时可以自己定义:
代码 10-21:定义并使用自定义异常
class ScoreError(Exception): # 继承内置的Exception类
"""成绩不合法时抛出的异常"""
pass
def check_score(score):
if score < 0 or score > 100:
raise ScoreError("成绩必须在0~100之间,当前值:" + str(score))
return score
# 正常使用
check_score(85) # 合法,平安通过
# 不合法的成绩
try:
check_score(120)
except ScoreError as e:
print("录入被拒绝:", e) # 输出:录入被拒绝: 成绩必须在0~100之间,当前值:120定义自定义异常的固定套路只有两步:写一个类,继承Exception(第7章的继承知识即学即用——Exception是所有异常的"父类",子类自动获得异常的全部本领);类体通常一句pass即可,异常的名字本身就是最重要的信息。之后就能像内置异常一样,用raise抛出、用except接住,而且except ScoreError只拦截这一种业务错误,不会误伤其他异常——报错的"分类管理"就此实现。
5.2 装饰器
【拓展阅读】本小节为了解内容,不要求掌握,不作考核要求。
第5章说过,函数名可以像普通值一样赋给变量、当参数传递。这个特性到极致,就能写出"给函数加功能"的代码。比如想统计某个函数执行花了多久,普通写法是在函数前后各加一行计时;若十个函数都要计时,就要复制十遍——又是熟悉的重复代码问题。装饰器(Decorator)提供了一次封装、处处生效的解法:
代码 10-22:一个最小的计时装饰器
import time
def timing(func): # 接收一个函数作为参数
def wrapper(): # 定义一个"包了一层"的新函数
start = time.time() # 记录开始时间
func() # 执行原函数
end = time.time() # 记录结束时间
print(f"【{func.__name__}】耗时{end - start:.4f}秒")
return wrapper # 返回包装后的新函数
@timing # @语法:把下面的函数交给timing包装
def count_1_to_million():
total = 0
for i in range(1000001):
total += i
print("1加到1000000的和:", total)
count_1_to_million()程序运行效果如下:
1加到1000000的和: 500000500000
【count_1_to_million】耗时0.0312秒@timing这行是Python的语法糖,它等价于count_1_to_million = timing(count_1_to_million)——把原函数交给timing,换回一个"先计时、再干活"的包装版本。从此任何函数只要顶一行@timing,就自动获得计时功能,原函数一行不改。
理解到"@就是把函数送去包装再换回来"这个层面即可。装饰器背后的闭包原理、带参数的装饰器等深入话题,等将来学到框架开发(那里的登录校验、日志记录大量使用装饰器)时再回头补课不迟。此刻你在别人的代码里看到@,只要知道"它在给函数附加功能"就够了。
到本章为止,这本讲义的全部内容就画上句号了。从第一章打印出"Hello World",到第十章弹出自己的窗口,我们沿着"成绩管理"这条主线,走过了分支循环、容器、函数、类、模块、文件、数据库,又在拓展篇看了绘图、图形界面与进阶写法。Python的世界远比这九章十节辽阔——网络爬虫、数据分析、人工智能、Web开发,每一扇门都已经在脚下垫好了台阶。带上这门课给你的基本功,继续往前走吧:人生苦短,我用Python。
