☰
Python开源库 Streamlit 应用案例
2026/10/13 3:39:40 网站建设 项目流程

前言


学 Streamlit 的人常常在同一个地方卡住:为什么我在按钮回调里改了变量,页面上却还是旧值?为什么我明明只动了滑块,整个页面的数据又重新加载了一遍?为什么别人写的例子里到处是st.session_state和@st.cache_data,看着像在解决什么问题,我却不知道问题在哪?


这些疑惑的答案只有一个,而且只要抓住它,Streamlit 的其余设计都变得顺理成章:Streamlit 的脚本是「每次交互都从头重跑一遍」的。你拖动滑块、在输入框敲字、点一下按钮,Streamlit 不是去修改页面的某个局部,而是把整个脚本重新执行一次,然后拿新结果去更新页面。


本文就围绕这一条心智模型展开。先讲清「重跑」到底意味着什么,再由它推出两个必然结果:普通变量会丢,所以要st.session_state;重复计算会白做,所以要@st.cache_data。最后给一个能跑的完整例子。


Streamlit 是第三方库,需要安装,版本与参数以官方文档为准。


一、理解 Streamlit 的钥匙:每次交互都从头重跑


写一个最简单的 Streamlit 应用:


# 适用于 Python 3.8+;需先安装 streamlit
import streamlit as st

st.title("重跑演示")
name = st.text_input("你的名字")
st.write("你好," + (name or "陌生人"))

运行方式是命令行执行streamlit run 你的文件名.py。打开页面后,你在输入框里打字,会看到输出实时跟着变。


关键问题来了:这段代码在页面生命周期里执行了不止一次。页面第一次加载执行一次;你每敲一个字符,Streamlit 就把整个脚本再执行一次。也就是说,st.text_input这一行被反复调用,st.write也被反复调用。你看到的页面内容,永远是「最后一次完整执行」的结果。


理解了这一点,很多怪现象就说得通了:



  • 页面上看到的所有元素,都是当前这次执行按顺序输出的。

  • 你在脚本里定义的普通变量,每次执行都重新诞生一次。

  • 如果你的脚本里有耗时操作(读数据库、拉接口),它会在每一次交互时都被执行。


也正因为如此,Streamlit 里没有「回调触发后修改页面」这种思路——没有页面可以修改,页面只是脚本输出的一帧。


二、重跑带来的第一个问题:变量会丢


先看看哪些东西会丢、哪些不会,这个区分很重要。




对象重跑后会怎样原因



普通局部变量重新初始化脚本重跑,赋值语句又执行了一遍

控件的值自动保留Streamlit 按控件标识记住了用户的选择

st.session_state里的值保留它就存在「会话」这一层,跨重跑存在

@st.cache_data缓存的结果保留缓存存在于会话之上,可跨用户跨重跑



也就是说,你不用为控件的值操心:st.text_input("你的名字")重跑之后依然会返回上一次输入的内容。但你自己算出来的中间量,重跑后就没影了。


最典型的场景是计数器。下面这段是错的:


# 适用于 Python 3.8+;需先安装 streamlit(❌ 反例,仅示意问题)
import streamlit as st

count = 0
if st.button("加一"):
count += 1
st.write("当前次数:", count)

每次点按钮,脚本重跑,count = 0先把它清成 0,再加 1,结果永远是 1。正确做法是把它放进st.session_state:


# 适用于 Python 3.8+;需先安装 streamlit
import streamlit as st

if "count" not in st.session_state:
st.session_state["count"] = 0

if st.button("加一"):
st.session_state["count"] += 1

st.write("当前次数:", st.session_state["count"])

st.session_state是一个在重跑之间存活的字典,官方文档的说法是「一种在重跑之间共享变量的方式,针对每个用户会话」。注意st.write(st.session_state)还能直接把里面的内容全部显示出来,调试时很好用。


它一个重要特性是:每个用户有自己的一份。两个同事同时打开这个页面,各自点各自按钮,计数互不干扰——这正是「会话状态」的含义。


三、重跑带来的第二个问题:重复计算


既然每次交互都重跑,那么「读一个大 CSV」「调用一次慢接口」这类操作就会被反复执行。你拖动一次滑块,接口就被调一次,这显然不能接受。


Streamlit 的解法是缓存:给函数加上@st.cache_data装饰器,Streamlit 会记住「同样的参数调用过」,下次直接返回缓存结果,不再执行函数体。


# 适用于 Python 3.8+;需先安装 streamlit
import streamlit as st


@st.cache_data
def load_sales(year, region):
# 这里放真实场景中耗时的读取或计算
return {"year": year, "region": region, "total": year * 10}


data = load_sales(2026, "华东")
st.write(data)

理解缓存有几个要点:



  • 缓存以参数为键。参数一样就用缓存,参数变了就重新算。所以要求参数是可哈希的;官方文档说明,若对象的哈希方法不适用,回退方案还要求它能被pickle序列化。

  • 缓存的返回值是副本。官方文档明确指出,st.cache_data返回的是拷贝;如果你要缓存一个需要就地修改的共享资源(比如数据库连接、模型对象),应该用st.cache_resource而不是它。

  • st.cache_data取代了早期的@st.cache。看到老教程里的@st.cache,请按官方文档迁移到st.cache_data(返回值数据)或st.cache_resource(共享资源)。

  • 它还有ttl(过期时间)、max_entries等参数,具体签名以官方文档为准。


一句话:重跑保证了「页面上看到的永远是最新状态」,缓存保证了「最新状态不必从零算起」。两者配合才是完整的一套。


四、一个完整的小应用与常用控件


下面这个例子把两件工具都用上了:一个带缓存的「数据加载」,加上一个用会话状态维护的「浏览历史」。


# 适用于 Python 3.8+;需先安装 streamlit
import streamlit as st

st.title("销售简报")

# 缓存:参数相同就不重新计算
@st.cache_data
def load_months(year):
return [{"month": m, "amount": (m * 37 + year) % 500} for m in range(1, 13)]


# 会话状态:跨重跑保留的浏览历史
if "history" not in st.session_state:
st.session_state["history"] = []

year = st.selectbox("年份", [2024, 2025, 2026])
rows = load_months(year)

threshold = st.slider("只看金额大于", 0, 500, 200)
shown = [r for r in rows if r["amount"] > threshold]

st.write(f"共 {len(shown)} 个月份超过阈值")
st.dataframe(shown)

if st.button("记录这次筛选"):
st.session_state["history"].append({"year": year, "threshold": threshold})

st.write("筛选历史:", st.session_state["history"])

这段代码的行为值得顺着「重跑」推一遍:每次你动年份,脚本重跑,但load_months因为参数没变(或者变了之后重新算一次并缓存)不会做重复劳动;每次你点「记录」,脚本重跑,history因为存在会话状态里,追加的记录被保留了下来。


常用控件与布局速览




控件大致用途返回值



st.text_input单行文本字符串

st.text_area多行文本字符串

st.selectbox下拉单选选中项

st.multiselect下拉多选列表

st.slider数值区间拖动数值或区间

st.checkbox勾选框布尔值

st.button触发一次动作布尔值,仅本次重跑为真

st.file_uploader上传文件类文件对象



关于st.button有一个必须记住的细节:它返回True只在这一帧。也就是说,脚本重跑一次,按钮为真的判断只在「刚刚被点击后的那次重跑」成立,下一次重跑(比如你又动了滑块)它就回到False了。所以「点击后要做的事」必须写在if st.button(...):这一块里,不能指望它一直为真。


布局方面,st.columns可以把页面横向切成几列,st.sidebar把控件放到侧边栏,st.tabs做标签页。这些都是「输出到哪里」的容器,不影响重跑模型。


常见坑点


1. 用普通变量跨重跑保存状态


❌count = 0写在脚本顶层,指望按钮点一次加一次。 ✅ 放进if "count" not in st.session_state: st.session_state["count"] = 0,之后只读改它。


2. 把耗时调用放在缓存之外


❌ 模块顶层直接data = requests.get(...),每次交互都重新请求。 ✅ 包成一个函数并加@st.cache_data,让相同参数命中缓存。


3. 把@st.cache_data用在共享资源上


❌ 用@st.cache_data缓存数据库连接、模型对象。 ✅ 这类需要共享、需要就地使用的资源用@st.cache_resource;st.cache_data的返回值是副本。


4. 以为st.button会一直为真


❌ 在按钮判断之外还用同一个布尔值控制后续逻辑。 ✅ 点击相关的逻辑全部写在if st.button(...):块内,因为它的真值只维持这一帧。


5. 忘了给缓存函数参数考虑可哈希性


❌ 传一个不可哈希、也无法pickle的对象进被缓存的函数,运行时缓存直接失效或报错。 ✅ 只传简单、可序列化的参数;复杂对象在函数内部按标识自行处理。


6. 在循环里创建同名控件


❌ 循环中反复st.button("确定"),多个控件同名,行为互相干扰。 ✅ 每个控件给出唯一的key参数,把它和循环变量绑定。


7. 用老教程里的@st.cache


❌ 照抄早期资料的@st.cache,语义不清。 ✅ 按官方文档换成@st.cache_data(数据)或@st.cache_resource(资源)。


8. 拿st.write当万能调试器却看不到想看的东西


❌ 只st.write变量名,不看会话状态里到底有什么。 ✅ 直接st.write(st.session_state)把整个会话状态打出来,一眼看清哪些键在、值是什么。


总结




现象真正原因对策



改动一处,整页重建每次交互都重跑整个脚本接受它,把所有逻辑写成可重复执行的形式

变量改了又变回去普通变量每次重跑重新初始化放入st.session_state

慢操作被反复执行重跑时那段代码又被走了一遍用@st.cache_data缓存结果

共享对象被复制st.cache_data返回副本共享资源改用@st.cache_resource

按钮判断莫名其妙失效按钮真值只维持一帧逻辑写在if st.button(...):内



Streamlit 不难,难的是换脑子:不要再想「我改这个页面元素」,而要想「这次执行会输出什么」。把这条记住,st.session_state和@st.cache_data就不再是需要死记的 API,而是「重跑」这个模型的必然产物。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询