本章精髓 界面和逻辑是两件可以分开的事。同一套逻辑既能套一个命令行的壳,也能套一个窗口的壳。换壳不换芯,是程序能被不懂终端的人用起来的关键一步。
学完本章后,你应该能够:
- 指出一份脚本里哪几行属于逻辑、哪几行属于界面,并说明分界线画在哪里;
- 在提示词里划出不可改动区,保住已经跑通的那部分逻辑;
- 给一个命令行脚本加上窗口,包含文件选择、结果显示与状态提示;
- 为危险操作加上有效的二次确认,并把报错改写成使用者看得懂的提示;
- 说明窗口在长任务中停止响应的原因,并给出最简单的缓解办法。
成品 · 共用的二十九行 · 划出不可改动区
先看两个壳分别长什么样,再看它们共用的是什么。
1.1本章成品:同一个工具的两个壳
手上有一个批量重命名的脚本,已经在终端里跑通了。本章给它加一个窗口:点按钮选文件夹,在窗口里看到将要改成什么,点确认才执行,底下一行显示进度。
原来的样子。在终端里输入命令,看打印,输入 y 确认。
加上窗口之后。同样这几件事,改成点按钮完成。
窗口里显示的内容,与终端里打印的完全一致
1.2两个壳共用的是什么
两份文件放在一起对照,会发现中间那二十九行完全相同:算出每个文件将要改成什么名字的那一段,以及按清单真正改名的那一段。换壳的过程中,这二十九行一个字都没有动。
do_rename
壳负责与人打交道,芯负责把事情算对
打开 第12章演示_三个壳.html,同一段逻辑外面换三种壳,看哪一部分在变、哪一部分一动不动。
1.3为什么值得多做一个壳
命令行版本已经能用了,功能一样也不少。多做一个窗口,改变的不是功能,而是使用者:从"你自己"变成"别人"。这一变化带来三件必须处理的事。
1.4本章新技巧:划出不可改动区
把一份已经跑通的脚本整个交给大模型,再要求它加界面,得到的往往是一份被重写过的新代码:变量改了名、循环换了写法、几个判断被合并。这些改动多数没有错,但它们把已经验证过的东西变回了未验证的东西,而你手上没有测试可以再验一遍。
所以提问时要先划出边界:哪一段一行都不许动,写清楚起止位置。剩下的部分才允许它自由发挥。边界划得越具体,它照做的可能性越高。写明"从 def preview_rename 到 do_rename 结束",比写"逻辑部分不要动"有效得多。
1.5可直接复制的提示词
主提示词里的几条要求,分别对应后面要讲的知识点:
1.6大模型的常见偏差
未经要求就重写了逻辑函数。 这是本章最需要防的一种。它会觉得原来的写法可以更简洁,于是改掉几行。改完之后功能大概率仍然正确,但你已经无法确定它和原来完全等价。"一行都不要动"这句必须写,并且要写明起止位置。
引入需要安装的界面库。 它常会用比 tkinter 更现代的界面库,做出来确实好看。代价是要先 pip 安装,机房网络下可能装不上;即便装上,文件体积也会明显变大。
把逻辑和界面重新搅在一起。 常见的形式是:它在逻辑函数里直接加一句弹窗,或者直接把结果写进文本框。这样改完窗口版能跑,命令行版却坏了,而且下次再换壳还要重来。验收时要专门检查这一点。
确认框写成一句"确定吗"。 它默认给的确认文案往往不含具体信息。要求里要写明"写清将要改动多少个文件",否则这个确认框等于没有。
第12章演示_窗口版.py。它的逻辑区与 第12章演示_命令行版.py 逐字符相同,可以直接用于对照检查。装不上 tcl/tk 的机器,用 第12章演示_窗口沙盘.html 代替:括号笔误、界面卡死、确认框的两种写法,都能在网页里当堂演一遍。所有演示文件只用标准库,不依赖网络。函数 · 参数与返回值 · 接口 · 分离
本章的核心内容集中在这一部分和下一部分。
这八个术语会原样出现在界面库文档、交互规范与缺陷记录里。前四个在本部分讲解,后四个在第三部分讲解。
2.1四个知识点,一个一个看
同一个名字,两处调用,代码只有一份
现象:代码里出现了 def preview_rename(folder, prefix): 这样的一行,下面缩进的几行是它的内容。
概念:函数(function)就是给一段代码起个名字。别处写上这个名字加一对括号,就是调用它。分离之所以能成立,靠的就是这一点:逻辑写成函数,壳只负责按名字调用,不必知道里面是怎么算的。
类比:菜单上写"宫保鸡丁"三个字,点菜的人不必知道后厨怎么做。
括号里填的是材料,return 交回来的是成果
现象:函数名后面的括号里写着 folder, prefix,函数最后一行是 return plan。
概念:括号里的名字叫参数(parameter),是这段代码干活需要的材料,调用时填什么它就用什么。return 交回去的东西叫返回值。交回去之后由谁显示、怎么显示,这个函数一概不管。正是这一点让窗口壳能把同一份清单放进文本框,而命令行壳把它打印出来。
类比:把布料和尺寸交给裁缝,拿回一件衣服。衣服穿去哪里,裁缝不问。
这三项就是接口,改任意一项,两边都要一起改
现象:提示词里那句"不要给它们改名或加参数"。
概念:接口(interface)是两部分代码之间约定好的那几件事:名字、参数、返回值的形状。接口不变,两边就可以各自改各自的。所以"不许动逻辑"这句要求,实质上要保的不是那二十九行的写法,而是这三项约定。改名和加参数看起来只是小改,其实是在改约定。
类比:插头和插座。形状对得上,电器和电网各自换代都不影响使用。
换壳时只动上面一层,下面一层不动
现象:两份文件的中间二十九行完全相同,不同的只有上下两头。
概念:界面逻辑分离指把"与人打交道"的部分和"把事情算对"的部分写在不同的位置,两者只通过接口互相调用。分离之后,界面可以换、可以重做,逻辑不必跟着改;反过来,逻辑出错也不会牵连到界面。判断分离得好不好的办法很简单:换一个壳,逻辑要一行都不用改。
类比:同一台发动机可以装进不同的车壳。换壳的时候,发动机不必重造。
2.2先看命令行版:逻辑与外壳已经分开了
加窗口之前,这份脚本已经写成了两段。上半段是逻辑,下半段是命令行外壳,中间用一行注释隔开。
from pathlib import Path
# ============ 逻辑区 · 换壳时一行都不要动 ============
def preview_rename(folder, prefix):
# 预演:算出每个文件将要改成什么名字,返回一份清单,不改动任何文件
plan = []
number = 0
for old in sorted(p for p in Path(folder).iterdir() if p.is_file()):
if old.suffix.lower() not in ['.jpg', '.png']:
continue
number = number + 1
new = old.with_name(prefix + '_' + str(number).zfill(3) + old.suffix.lower())
if new.exists() and new != old:
new = old.with_name(new.stem + '-2' + new.suffix)
plan.append((old, new))
return plan
def do_rename(plan, on_progress=None):
# 执行:按清单真正改名,返回成功条数
# 每改完一个就调用一次 on_progress,由外壳自己决定怎么显示进度
done = 0
for old, new in plan:
old.rename(new)
done = done + 1
if on_progress is not None:
on_progress(done, len(plan))
return done
# ============ 命令行外壳 · 窗口版会把这一段整个换掉 ============
def print_progress(done, total):
print(' 已处理', done, '/', total)
if __name__ == '__main__':
FOLDER = '测试文件夹'
PREFIX = '课堂照片'
folder = Path(__file__).with_name(FOLDER)
plan = preview_rename(folder, PREFIX)
print('预演结果,共', len(plan), '个文件将被改名:')
for old, new in plan:
print(' ', old.name, '→', new.name)
answer = input('确认执行请输入 y,直接回车取消:')
if answer.strip().lower() == 'y':
done = do_rename(plan, print_progress)
print('完成,共改名', done, '个文件。')
else:
print('已取消,没有改动任何文件。')
input('按回车键结束')
按本章方法核对:输入是文件夹与前缀两项,输出是屏幕上的预演清单与进度,重复执行的部分在两个函数内部各有一处。整份文件里真正与"命令行"这个壳有关的,只有最下面那十几行。
tkinter · 点了才发生 · 回调 · 有效确认
窗口程序不从上往下跑完就结束,它搭好界面之后停下来等。
3.1另外四个知识点
窗口、按钮、输入框、状态栏,都由界面库画出来
现象:代码第二行 import tkinter as tk,运行之后屏幕上出现一个窗口。
概念:窗口、按钮、输入框这些成品部件由图形界面库(GUI library)提供。tkinter 随 Python 一同安装,不需要另行下载,画出来的窗口也谈不上好看,但胜在到处都能跑。其他界面库功能更强,代价是要额外安装,并且体积可观。本课程一律用 tkinter,理由只有一条:不必安装。
类比:家里现成的工具,样子普通,但拿来就能用,不必出门去借。
窗口程序大部分时间在等,不在跑
现象:代码最后一行是 root.mainloop(),运行到这里就停住了,窗口却一直开着。
概念:命令行程序从第一行跑到最后一行就结束。窗口程序不同:它先把界面搭好,然后停在原地等待,使用者点了哪个按钮,才去执行对应的那一段代码,这种方式叫事件驱动(event-driven)。command=on_run 这样的写法就是登记:这个按钮被点的时候,去执行这个名字对应的那段代码。
类比:按门铃才响。门铃平时并不工作,它只是接着,等人来按。
同一个 do_rename,三种进度显示方式
现象:do_rename(plan, print_progress) 这一行,第二个材料填的是一个函数的名字。
概念:把一个函数当作材料交给另一个函数,让对方在合适的时候回头来调用它,交进去的这个函数叫回调(callback)。逻辑区只负责在每改完一个文件时喊一声,喊出来的话显示在哪里,由壳自己决定。注意 print_progress 后面没有括号,交进去的是这个函数本身,不是它的结果。
类比:寄快递时留一个电话号码,让对方到了再打给你。号码是留给他的,什么时候打由他定。
这一步没有撤销,确定继续吗?
数量写进提示里,比只写"确定吗"有用得多
现象:点「确认执行」之后没有立刻开始,而是先弹出一个问句。
概念:会造成不可撤销后果的操作,执行前应当再问一次,这一步叫二次确认(confirmation)。有效的确认要给出具体信息:动的是什么、有多少、能不能撤销。只写"确定吗"的提示框,使用者点得越熟越不看,形同虚设。
类比:删除文件时系统问的那一句。问得具体,人才会真的停下来看一眼。
3.2窗口外壳怎么调用同一套逻辑
窗口版把上面那二十九行原样抄了过来,只重写外壳。外壳做三件事:搭界面、登记按钮、在被点击时调用逻辑。
# ① 点「选择文件夹」时执行:弹出系统对话框,选完立即预演
def on_choose_folder():
picked = filedialog.askdirectory(title='选择要处理的文件夹')
if not picked: # 使用者点了取消
return
current_plan = preview_rename(Path(picked), prefix_entry.get().strip())
result_box.delete('1.0', tk.END)
for old, new in current_plan:
result_box.insert(tk.END, old.name + ' → ' + new.name + '\n')
# ② 点「确认执行」时执行:先二次确认,再调用逻辑区的 do_rename
def on_run():
agreed = messagebox.askyesno('确认执行',
'将要改动 ' + str(len(current_plan)) + ' 个文件的名称。\n\n这一步没有撤销,确定继续吗?')
if not agreed:
return
try:
done = do_rename(current_plan, update_progress)
except Exception as error:
messagebox.showerror('执行中断',
'出错的文件已停下,后面的文件保持原样。\n\n' + str(error))
return
messagebox.showinfo('完成', '共改名 ' + str(done) + ' 个文件。')
# ③ 登记:这个按钮被点的时候,去执行 on_run 这一段
tk.Button(top, text='确认执行', command=on_run).pack(side='left')
# ④ 搭完界面之后停在这里等,下面不会再有代码被执行
root.mainloop()
3.3登记的时候不要加括号
command=on_run 交进去的是这段代码本身,等按钮被点了才执行。写成 command=on_run() 的话,界面刚搭好它就立刻执行了一遍,而按钮反倒变成点了没反应。两个症状同时出现,就是这一处。打开 第12章演示_窗口沙盘.html,把"登记时多加一对括号"这个开关打开,看窗口刚出来时发生了什么,再点一下按钮。这个网页里的其他几个开关,对应的都是本章要防的错误。
把两份文件并排一看,会发现窗口版里没有任何一行在算文件该改成什么名字。它只是把使用者的动作翻译成对那两个函数的调用,再把结果翻译成屏幕上的文字。这就是"壳"这个词的意思。
3.4给普通人用的三个细节
功能做完之后,还有三处只有换了使用者才会暴露的问题。
路径由系统对话框给出,不必由人输入
什么样:点「选择文件夹」按钮,弹出的是系统自带的那个熟悉的选择框。
为什么要管:文件对话框是操作系统提供的成品部件,程序只要一行代码就能弹出它,选完拿回一个路径。好处有两个:使用者不必知道路径怎么写,程序也不会拿到一个拼错的路径。使用者点了取消时,拿回的是空值,这种情况必须单独处理,否则下一步就会报错。
程序没有死,只是没有空闲去重画窗口
什么样:点了执行之后窗口整个变白,鼠标点哪里都没反应,过几秒才恢复。
为什么要管:窗口要靠程序抽空重画。一段代码如果一口气跑很久不歇手,窗口就得不到重画的机会,看上去像是死了,这叫界面无响应。最简单的缓解办法是每处理完一个就更新一次状态、让窗口重画一下,这正是回调的用处。任务真正很长时才需要另开线程,本章不涉及。
同一个错误,两种说法,读者不同
什么样:同一个错误,在终端里是一屏英文,在窗口里是一句中文。
为什么要管:报错原文是写给开发者的定位信息;面向用户的错误提示要回答另外三个问题:出了什么事、现在是什么状态、接下来该怎么办。本章的做法是让逻辑函数照常抛出错误,由外壳接住并改写。逻辑不管怎么显示错误,这样它换个壳仍然能用。
跑通命令行 · 加窗口 · 对照检查
从一个能跑的命令行脚本开始,到一个可以交给别人用的窗口。
4.1演示步骤
4.2前沿三分钟:核心与界面分开的那条线
每章固定栏目 · 一条与本章内容相关的行业动态
本章这种结构在实际项目中相当常见。版本控制工具既有命令行也有多种图形客户端,数据库既有命令行客户端也有图形管理工具,它们共用的都是同一套核心。这类做法的正式说法是核心与前端分离,也有人称之为无头(headless):核心本身不带界面,界面另做一层,甚至可以做好几层。
这样安排的好处在换代时才看得出来。界面的样式与交互习惯几年一变,核心的算法却往往能用很多年。分开之后,界面重做几轮,核心不必跟着重写;反过来,核心升级也不必等界面配合。近年常见的形式还包括把核心做成一个可供调用的服务,界面、脚本、别的程序都能接上去。
判断分离得彻不彻底,业内有个朴素的检验办法,和本章的做法完全一致:再写一个壳,看核心要不要改。一行都不用改,才算分开了。
三选一 · 六步 · 两个壳都要能跑
上机环节由此开始,当堂完成并提交。
5.1三个题目,任选其一
把批量改名脚本加上界面:选文件夹、看预演、确认执行。
通用选一个文件、点一个按钮,直接在窗口里看到结论,不必看终端。
偏文科把一个数据处理脚本加上界面:选文件夹、显示统计结果、导出。
偏理工三个题目的共同要求是:先有一个能跑通的命令行版本,再加窗口;加完之后两个版本都要能跑。手上没有现成脚本的,可以直接用演示件里的命令行版作为起点。允许更换题材。
5.2上机六步
5.3共同验收标准
5.4常见错误与处理
root.mainloop()command=on_run(),多了一对括号command=on_run5.5提交内容
提交方式:按教师提供的作业模板填写,文档开头注明所选题目。提交渠道见课堂通知,当堂提交。
5.6再进一步(选做)
给窗口加一个「撤销上一次」按钮:执行时把新旧名称记在一份文件里,点撤销就按记录改回去。
做的时候会碰到一个需要判断的问题:撤销这个动作本身,算逻辑还是算界面,需要你自己判断。按本章的分法,它应当写成逻辑区的第三个函数,由按钮调用;但这样一来,命令行版也应该能撤销,外壳那一段也要跟着补。功能加在哪一层,决定了它将来能被谁复用。
本章五条结论
- 界面与逻辑是可以分开的两件事。逻辑写成函数,壳只负责按名字调用。检验办法是换一个壳,看逻辑要不要改。
- 接口是名字、参数、返回值这三项约定。改名和加参数看起来是小改,其实是在改约定。
- 窗口程序不从上往下跑完就结束,它搭好界面之后停下来等。点了哪个按钮,才执行对应的那一段。
- 使用者从自己变成别人时,要补三样东西:路径改成对话框选、危险操作加二次确认、报错改写成中文提示。
- 给已跑通的代码加东西时,先划出不可改动区,写明起止位置。已经跑通的代码是一份资产。