执行模型 — 为什么应用没有记忆,以及 st.session_state 怎么给它造一个
执行模型 — 为什么应用没有记忆,以及 st.session_state 怎么给它造一个
这一章回答全书最要紧的一个问题:你的 Streamlit 应用为什么没有记忆? 答案是它的执行模型:界面一变,整个脚本从头重跑,普通变量每轮清零。 这一章用一个小应用连踩四个坑,每踩一个坑,就从执行模型里挖出一件武器。 读完你会自己诊断「变量怎么没了」「删除怎么慢一拍」「应用怎么卡死了」这三类 Streamlit 最典型的故障。
1. 这一章讲什么
前两章写的应用有个共同点:用户每一步动作都自包含,应用不需要「记住」之前发生过什么。 这一章的应用不一样——待办清单(sidebar 输入任务,主区显示任务列表, 能勾选完成、能删除,顶部显示完成进度)1。 它必须跨多轮交互记住一份数据,而 Streamlit 的执行模型偏偏每轮都清空内存。 作者故意不走顺路:这一章「我们会在构建应用时真的遇到错误,再靠理解 Streamlit 去修它们」2——四个坑,一个比一个深,最后一个是无限循环。
2. 顶层全景:四个坑与三件武器
坑 ① 循环里放了两个一样的 Delete 按钮 → 武器① widget key:给部件一个唯一身份
坑 ② 加第二个任务时,第一个消失了 → 武器② st.session_state:跨轮存活的存储
坑 ③ 点 Delete 没反应,再点别的才删 → 武器③ st.rerun:代码里主动喊「从头再来」
坑 ④ 接上 checkbox 后应用卡死(RUNNING…) → 教训:st.rerun 必须有退出条件
图说:坑是顺着挖的,每件武器都是为了填前一个坑,最后一件是「别把武器用过头」。应用的设计本身很简单(任务 = 名字 + 是否完成;四个动作:加、标完成、取消完成、删), 界面按 03 章的老规矩画草图3。真正的内容全在「哪里会塌、为什么塌」。
3. 核心原理(一):widget key——部件的唯一身份
先搭显示部分:循环 for task in task_list: 里每轮画一个 checkbox,
用 st.columns([0.8, 0.2]) 分出宽 8 : 2 的两列(注意这次传的是宽度比例列表,
不是列数;等宽对「任务文字任意长、按钮很短的场景不合理)4,
左列放 checkbox,右列放 Delete 按钮。
运行,报错:「duplicate widget keys」5。
为什么: Streamlit 内部用 key(键)标识每个部件。你没给时它自己生成, 规则大致是「按部件的特征生成」——按钮的特征就是按钮上的文字。 两个文字都是 Delete 的按钮,自动 key 一模一样,Streamlit 分不清谁是谁,直接报错6。
修法: 手动给 key,并保证唯一。循环里最顺手的唯一来源是下标:
if delete_col.button("Delete", key=f"delete_{idx}"):,
第一个任务的按钮是 delete_0,第二个是 delete_17。
顺带认识 enumerate——它一次给你「下标 + 元素」两样,
不用写 for idx in range(len(task_list)): task = task_list[idx] 这种笨办法8。
Checkbox 当时没报错,是因为标签(任务名)天然不同。 但只要列表里出现两个重名任务,
同样的错立刻冒出来——所以 checkbox 也补上 key:key=f"task_{idx}"9。
经验法则:凡是在循环里生成的部件,一律显式给 key。
4. 核心原理(二):执行模型——脚本重跑,变量清零
接上动作函数(add_task 追加、delete_task 按下标删、mark_done / mark_not_done 改状态), 加上「输入框 + Add task 按钮」的 sidebar。运行,输入 Clean garage,点 Add——出现了。 再输入 Finalize project proposal,再点 Add——第一个任务消失了。 点某个 checkbox,任务也消失。没有报错,就是东西不见了10。
调试第一步:看见变量的值。 print 在浏览器里看不到,Streamlit 的对应物是
st.info(f"task_list: {task_list}")——一个蓝框,和 st.success/st.error/st.warning 同族11。
st.info 显示 task_list: [] 永远是空,或者只装最新一个任务。
结论:显示逻辑没错,是 task_list 本身每轮都被重置了12。
为什么会这样——执行模型,三句话说清:
- 你的应用 = 一台 Streamlit 服务器(常驻进程,监听一个端口; 端口可以想象成公司分机号,让网络请求找到正确的程序)13 + 浏览器里的前端。
- 服务器收到请求(页面打开、任何一次点击、下拉换项、滑块拖动……)的响应, 是把你的 Python 脚本从头跑到尾,然后把「页面该长什么样」的更新发回浏览器14。 前端检测到点击 → 通知服务器 → 服务器重跑脚本 → 把差异发回前端——这个循环对 任何交互都成立,不只是按钮15。
- 既然每轮都是一次全新的脚本执行,写在顶层的
task_list = []每轮都执行一次—— 上一轮的列表直接被丢弃。
用时间线走一遍那个 bug,你就再也忘不掉这个模型:
第 1 轮(打开页面) task_list = [] → 按钮没被点 → 循环不执行 → 页面空白
第 2 轮(输入 Clean garage,点 Add)
task_list = [](又被重置!)→ text_input 这次带着 "Clean garage"
→ st.button 为 True(刚被点)→ add_task → task_list = [Clean garage]
→ 循环画出 1 个 checkbox ✔ 这轮是对的
第 3 轮(点 checkbox) task_list = [](又被重置!)→ st.button 为 False!
(按钮只在「紧跟着点击的那一轮」为 True,之后永远回到 False)
→ add_task 不执行 → 列表空 → 页面空白 ✖
图说:第 2 轮能成功纯属侥幸——重置发生之后、按钮恰好又替你把任务加了回去。
任何不点 Add 的交互,都会让列表裸奔。书里有一句点睛的描述:这个应用像金鱼,对上一轮没有任何记忆16。
5. 核心原理(三):st.session_state——跨轮存活的字典
解法是 st.session_state:一个跨重跑存活的数据容器。
session(会话)指「打开应用到刷新或关闭」这段时间;同一应用开两个浏览器标签,
就是两个独立会话,各有一份 session_state17。
用法几乎和字典一样,外加一个点号写法:
if "task_list" not in st.session_state: # 第一次运行才初始化
st.session_state.task_list = []
task_list = st.session_state.task_list # 后面代码照旧用短名字(照原书 listing 4.7。)两个细节都是坑位:初始化必须包在 if "task_list" not in ... 里——
否则每轮都重新赋空值,等于又回到金鱼状态18;把 st.session_state.task_list
赋值给一个本地变量 task_list,其余函数不用改,代码不臃肿19。
书里还给了个十行的玩具例子帮你建立直觉:一个 Increment 按钮,
每点一次 st.session_state.number += 1,number 存在 session_state 里,
点五次显示 5——同样的代码用普通变量写,永远显示 020。
改完这两行,待办应用第一次能用了:任务记得住,勾选状态记得住21。
6. 核心原理(四):st.rerun 与「慢一拍」之谜
接好 Delete:if delete_col.button("Delete", key=...): delete_task(idx)。
点一下——没反应。再点一下,或随便点个别的东西,任务才消失。
而且如果删的是中间的,先消失的是下一个任务22。
这又是执行模型,这次是「显示已经画完了」。 按时间线走(假设列表有三项, 点第三项 Buy bread 的 Delete):
第 1 轮:循环 idx=0 画按钮 delete_0(False)→ idx=1 画 delete_1(False)
→ idx=2 画 delete_2(False)→ 结束。页面三项齐全。
第 2 轮(刚点了 delete_2):idx=0、1 照旧画;False、False
→ idx=2:delete_2 这一轮为 True → delete_task(2) 执行,Buy bread 被删
→ 但!idx=0、1 的按钮和 checkbox 已经画在页面上了,
画它们依据的还是旧 task_list → 页面看起来什么都没变
第 3 轮(用户又点了任意东西):这轮循环从头到尾基于新列表 → Buy bread 终于消失
图说:删除发生在「显示循环跑到一半」的时候;没有下一次重跑,页面就停留在旧数据上。
「删中间的反而删了下一个」是下标位移:列表缩短后,下一个任务的 idx 变成了被删者的 idx,
显示循环拿着旧下标继续走,跳过了它。修法:删掉之后立刻主动重跑一轮。
st.rerun() 的意思是「立刻停止当前这次运行,从头再来」23:
if delete_col.button("Delete", key=f"delete_{idx}"):
delete_task(idx)
st.rerun()改完,点 Delete 立刻生效24。checkbox 的「勾选完成 + 划线」同理:
加上 st.rerun 才即时;不加就要等你下一次操作25。
(strikethrough 用的是 Markdown(给纯文本加格式标记的一种写法)的语法:标签里写 ~~任务名~~ 就是划线,
checkbox 的 label 支持 Markdown26。)
7. 核心原理(五):无限重跑循环——st.rerun 的反噬
给 checkbox 也照搬「动作 + st.rerun」的写法:
if task_col.checkbox(label, task.is_done, key=f"task_{idx}"):
mark_done(task)
st.rerun()
else:
mark_not_done(task)
st.rerun()保存,运行,应用卡死:整个页面灰掉,顶部 RUNNING… 不停27。
走一遍就知道为什么会死。 假设有一项未完成的任务:
第 1 轮:checkbox 为 False → 走 else → mark_not_done(它本来就是 False,
纯属白干)→ st.rerun()
第 2 轮:从头开始,checkbox 还是 False → 走 else → mark_not_done → st.rerun()
第 3 轮:……永无止境
图说:if/else 两个分支都无条件 st.rerun,而「未勾选」这个状态在每一轮都成立,
于是每一轮都自己触发下一轮——没有人踩刹车。修法:给 st.rerun 加条件——只在「状态真的要变」的时候才喊重跑28:
checked = task_col.checkbox(label, task.is_done, key=f"task_{idx}")
if checked and not task.is_done: # 勾了,而且之前没完成 → 状态要变
mark_done(task)
st.rerun()
elif not checked and task.is_done: # 取消了,而且之前完成了 → 状态要变
mark_not_done(task)
st.rerun()
# 其余情况(状态没变):什么都不做,循环自然走完,不触发重跑这才是正确的通式:动作函数 + st.rerun 只在「新旧状态不一致」的分支里出现。
加上最后的 st.metric 进度(3/5 done 形式),应用收工29。
8. 作者的判断与证据
判断:刻意用「踩坑驱动」教执行模型。 这一章不调顺路,每修一个 bug 才给一件武器30。 这是我们非常认可的教学决定——执行模型这种「反直觉但自洽」的机制, 顺讲十遍不如踩坑一遍;四个坑的梯度(报错 → 数据丢失 → 慢一拍 → 死循环) 恰好是从浅到深的完整证据链。
判断:本章放弃第 03 章的前后端分离等最佳实践,换更短的代码31。 合理——这一章的读者负担已经在执行模型上了,结构洁癖会让主线失焦。 但它同时暴露了 Streamlit 小应用的真实生态:很多真实 Streamlit 代码就是这样单文件、 全局变量、依赖 session_state 的,第 03 章那套结构在 Streamlit 世界里反而少见。
9. 边界与局限
- session_state 的生命周期只到刷新为止。 本章明确把「刷新后找回任务列表」 列为范围外32;数据要活得比会话久,得接外部存储(第 08 章)。
- 「按钮只在紧随其后的那一轮为 True」是行为约定而非必然——它是 Streamlit 的设计选择, 书中以事实陈述,没解释为什么这样设计(合理的猜测:避免一个点击被重复执行副作用, 但书里没有这么说,我们只标注为猜测)。
- 无限循环的检测只靠「应用不动了」。 Streamlit 不会提前警告你写了一个必然死循环的 分支结构;只能靠纪律:凡是 st.rerun,必配退出条件。
- st.rerun 是新手的第一号「卡死源」,但书里给的判据很朴素(状态变没变); 更复杂的状态机(多个部件互相触发)需要的纪律比本章示范的更多。
- st.info 调试大法在页面复杂时会污染界面,而且它显示的是「这一轮」的值—— 在「慢一拍」类 bug 里,它显示的恰好是误导你的那个旧值。
判断(我们的,不是书里的): 这一章是全书的机制中枢,而且它的讲法 有一个难得的品质:每一个抽象概念(key、session_state、rerun)都是从一个具体的 「页面坏了」的现象里逼出来的,不是先给定义再找例子。这正是「先现象后原理」的 教科书式执行。对编程语言研究,这一章是一个绝佳案例:Streamlit 用「无状态脚本 + 会话存储 + 显式重跑」三件套,把传统 GUI 编程里的事件回调模型整个换掉了—— 表达力下降,心智负担跟着下降,代价就是本章这四个坑。 如果错,会错在: 「踩坑驱动」对自学者要求很高——如果读者没有亲手跟着踩, 直接读结论,四个坑会退化成四个教条(「循环里要给 key」「变量要放 session_state」), 机制层面的理解反而不如顺讲的教材。这一章的价值高度依赖「动手」, 这点书里有劝(作者劝你 tinkering33),但对只读文档的人帮不上。
10. 可带走的
- 执行模型一句话:界面一变,脚本从头重跑;顶层变量每轮清零。 记住这一句, Streamlit 一半的古怪行为都能推出来。
- 服务器是应用的本体,前端只是投影;端口 = 分机号,8501 = 默认分机。
- 循环生成的部件必须显式给
key;Streamlit 自动 key 按部件特征(按钮的文字、checkbox 的标签)生成,重复即报错。 - 金鱼比喻:没有 session_state 的应用对上一轮毫无记忆。
st.session_state用法 = 字典用法 + 点号;初始化永远包if key not in st.session_state。- 会话 = 打开到刷新/关闭;两个浏览器标签 = 两个会话,两份 state。
- 调试 Streamlit 的第一工具是
st.info(print 的替身),但要记得它显示的是本轮值——慢一拍的 bug 里它是误导源。 st.button只在「紧跟着点击的那一轮」为 True;之后永远 False。- 「慢一拍」类 bug 的通式:状态在显示循环中途被改,页面停在旧数据;解法是动作后
st.rerun()。 st.rerun必须有退出条件:只在「新旧状态不一致」的分支里调用;否则无限循环、页面灰死。- st.columns 可以传宽度比例列表
[0.8, 0.2];st.button 的type="primary"用来突出主操作。 - Markdown 的
~~文字~~加划线,checkbox 标签支持 Markdown。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 待办清单概念与需求(持久化列为范围外) | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:43搜「adding tasks to a daily to-do list」) |
| 踩坑驱动的教学法声明 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:189搜「bumpier path」) |
| 应用设计:任务、动作、显示逻辑三分 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:135搜「three parts」) |
| st.columns([0.8, 0.2]) 宽度比例 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:344搜「relative widths」) |
| 重复 widget key 报错与手动 key | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:365搜「unique key」) |
| enumerate 讲解 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:378搜「On enumerate」) |
| 重名任务的 checkbox key 问题 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:389搜「two identical tasks」) |
| 加第二个任务,第一个消失 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:499搜「the old one disappears」) |
| st.info 替代 print 调试 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:515搜「variable values」) |
| 端口 = 分机号;服务器监听 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:557搜「extension number」) |
| 前端/服务器往返,脚本从头跑 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:575搜「in its」) |
| 任何交互都触发重跑 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:608搜「any interaction」) |
| 「按钮只在紧随点击的那轮为 True」 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:701搜「immediately after a click」) |
| 金鱼比喻与 dilemma 总结 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:727搜「goldfish」) |
| st.session_state 定义与会话含义 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:734搜「persist across reruns」) |
| session_state 的字典式用法与点号 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:761搜「almost exactly like a Python dictionary」) |
| Increment 玩具例子 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:776搜「number-incrementing」) |
| 为什么初始化要查存在性 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:800搜「run into the same problem」) |
| 接入 session_state 的两行改法 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:812搜「Adding session state」) |
| Delete 慢一拍的现象描述 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:894搜「lag between clicking」) |
| 逐轮执行图(删除时已画完) | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:900搜「deep dive」) |
| st.rerun 的定义 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:978搜「Quit the current run」) |
| checkbox 的 Markdown 划线标签 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:1007搜「markdown」) |
| 无限重跑循环的逐步推演 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:1103搜「infinite rerun loop」) |
| 带条件的修复(if/elif 只在状态变化时 rerun) | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:1149搜「Preventing the infinite rerun」) |
| 最终代码(listing 4.9)与 st.metric 进度 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:1184搜「completion progress indicator」) |
| 放弃最佳实践的声明 | 4 Streamlit’s execution model | text/13-ch04-4-streamlit-s-execution-model.txt:225搜「eschew some of the best practices」) |
Footnotes
-
出处:「4 Streamlit’s execution model」第 40–56 段(
text/13-ch04-4-streamlit-s-execution-model.txt:43,搜「adding tasks to a daily to-do list」)。需求六条、范围外三条(不持久化、不导出、不存历史)见同段。 ↩ -
出处:「4 Streamlit’s execution model」第 185–192 段(
text/13-ch04-4-streamlit-s-execution-model.txt:189,搜「bumpier path」)。 ↩ -
出处:「4 Streamlit’s execution model」第 85–134 段(
text/13-ch04-4-streamlit-s-execution-model.txt:88,搜「mock UI design」)。 ↩ -
出处:「4 Streamlit’s execution model」第 336–348 段(
text/13-ch04-4-streamlit-s-execution-model.txt:344,搜「relative widths」)。 ↩ -
出处:「4 Streamlit’s execution model」第 351–357 段(
text/13-ch04-4-streamlit-s-execution-model.txt:354,搜「multiple st.button widgets with the same key」)。 ↩ -
出处:「4 Streamlit’s execution model」第 360–363 段(
text/13-ch04-4-streamlit-s-execution-model.txt:361,搜「based on the widget's characteristics」)。 ↩ -
出处:「4 Streamlit’s execution model」第 364–374 段(
text/13-ch04-4-streamlit-s-execution-model.txt:367,搜「delete_0」)。 ↩ -
出处:「4 Streamlit’s execution model」第 378–385 段(
text/13-ch04-4-streamlit-s-execution-model.txt:378,搜「On enumerate」)。 ↩ -
出处:「4 Streamlit’s execution model」第 387–397 段(
text/13-ch04-4-streamlit-s-execution-model.txt:389,搜「two identical tasks」)。 ↩ -
出处:「4 Streamlit’s execution model」第 492–510 段(
text/13-ch04-4-streamlit-s-execution-model.txt:501,搜「the old one, Clean garage, is gone」)。 ↩ -
出处:「4 Streamlit’s execution model」第 514–530 段(
text/13-ch04-4-streamlit-s-execution-model.txt:519,搜「print statements don't appear」)。 ↩ -
出处:「4 Streamlit’s execution model」第 531–545 段(
text/13-ch04-4-streamlit-s-execution-model.txt:538,搜「task_list itself has lost」)。 ↩ -
出处:「4 Streamlit’s execution model」第 552–560 段(
text/13-ch04-4-streamlit-s-execution-model.txt:557,搜「extension number」)。 ↩ -
出处:「4 Streamlit’s execution model」第 564–572 段(
text/13-ch04-4-streamlit-s-execution-model.txt:568,搜「from top to bottom」)。 ↩ -
出处:「4 Streamlit’s execution model」第 574–611 段(
text/13-ch04-4-streamlit-s-execution-model.txt:608,搜「any interaction」)。 ↩ -
出处:「4 Streamlit’s execution model」第 727–730 段(
text/13-ch04-4-streamlit-s-execution-model.txt:727,搜「goldfish」)。 ↩ -
出处:「4 Streamlit’s execution model」第 732–738 段(
text/13-ch04-4-streamlit-s-execution-model.txt:736,搜「separate session」)。 ↩ -
出处:「4 Streamlit’s execution model」第 799–804 段(
text/13-ch04-4-streamlit-s-execution-model.txt:800,搜「run into the same problem」)。 ↩ -
出处:「4 Streamlit’s execution model」第 822–828 段(
text/13-ch04-4-streamlit-s-execution-model.txt:823,搜「tedious and clunky」)。 ↩ -
出处:「4 Streamlit’s execution model」第 776–798 段(
text/13-ch04-4-streamlit-s-execution-model.txt:776,搜「number-incrementing app」)。 ↩ -
出处:「4 Streamlit’s execution model」第 865–870 段(
text/13-ch04-4-streamlit-s-execution-model.txt:869,搜「voila」)。 ↩ -
出处:「4 Streamlit’s execution model」第 872–897 段(
text/13-ch04-4-streamlit-s-execution-model.txt:894,搜「lag between clicking」)。「删中间的,下一个消失」在同文件第 890–893 段。 ↩ -
出处:「4 Streamlit’s execution model」第 968–983 段(
text/13-ch04-4-streamlit-s-execution-model.txt:978,搜「Quit the current run」)。 ↩ -
出处:「4 Streamlit’s execution model」第 985–991 段(
text/13-ch04-4-streamlit-s-execution-model.txt:990,搜「It worked」)。 ↩ -
出处:「4 Streamlit’s execution model」第 1075–1086 段(
text/13-ch04-4-streamlit-s-execution-model.txt:1080,搜「lag of one user action」)。 ↩ -
出处:「4 Streamlit’s execution model」第 1007–1024 段(
text/13-ch04-4-streamlit-s-execution-model.txt:1011,搜「two pairs of tildes」)。 ↩ -
出处:「4 Streamlit’s execution model」第 1095–1101 段(
text/13-ch04-4-streamlit-s-execution-model.txt:1099,搜「stop responding entirely」)。 ↩ -
出处:「4 Streamlit’s execution model」第 1149–1178 段(
text/13-ch04-4-streamlit-s-execution-model.txt:1149,搜「Preventing the infinite rerun」)。 ↩ -
出处:「4 Streamlit’s execution model」第 1184–1200 段(
text/13-ch04-4-streamlit-s-execution-model.txt:1184,搜「completion progress indicator」)。 ↩ -
出处:「4 Streamlit’s execution model」第 185–192 段(
text/13-ch04-4-streamlit-s-execution-model.txt:191,搜「will push us to learn」)。 ↩ -
出处:「4 Streamlit’s execution model」第 224–227 段(
text/13-ch04-4-streamlit-s-execution-model.txt:225,搜「eschew some of the best practices」)。 ↩ -
出处:「4 Streamlit’s execution model」第 72–77 段(
text/13-ch04-4-streamlit-s-execution-model.txt:72,搜「persisting (saving) tasks」)。 ↩ -
出处:「4 Streamlit’s execution model」第 962–966 段(
text/13-ch04-4-streamlit-s-execution-model.txt:962,搜「Stepping through the execution」)。动手建议见第 2 章第 200–204 段。 ↩