Skip to content

Streamlit 状态管理与缓存 ​

1. Session State 详解 ​

1.1 什么是 Session State ​

Streamlit 的脚本式执行模型意味着每次交互都会从头到尾重新运行脚本。st.session_state 是在多次重运行之间持久化数据的机制。

python
import streamlit as st

# 基本用法
if "counter" not in st.session_state:
    st.session_state.counter = 0

st.write(f"计数器: {st.session_state.counter}")

if st.button("增加"):
    st.session_state.counter += 1
    st.rerun()  # 触发重新运行

1.2 初始化 Session State ​

python
import streamlit as st
import pandas as pd
import numpy as np

# 方式1:条件检查初始化
if "messages" not in st.session_state:
    st.session_state.messages = []

if "user_name" not in st.session_state:
    st.session_state.user_name = ""

# 方式2:使用字典批量初始化
def init_session_state():
    defaults = {
        "counter": 0,
        "messages": [],
        "settings": {
            "theme": "light",
            "language": "zh",
            "notifications": True
        },
        "data": None,
        "history": [],
    }
    for key, value in defaults.items():
        if key not in st.session_state:
            st.session_state[key] = value

init_session_state()

# 方式3:使用 st.session_state.update(推荐批量设置)
if "initialized" not in st.session_state:
    st.session_state.update({
        "counter": 0,
        "messages": [],
        "user": None,
        "preferences": {},
    })
    st.session_state.initialized = True

1.3 Session State 高级用法 ​

python
import streamlit as st
import pandas as pd
from datetime import datetime

# 多层嵌套字典
if "app_state" not in st.session_state:
    st.session_state.app_state = {
        "user": {
            "name": "张三",
            "role": "admin",
            "preferences": {
                "theme": "dark",
                "language": "zh"
            }
        },
        "data": {
            "current_page": "home",
            "filters": [],
            "sort_by": None
        }
    }

# 访问嵌套数据
st.write(f"用户名: {st.session_state.app_state['user']['name']}")
st.write(f"主题: {st.session_state.app_state['user']['preferences']['theme']}")

# 更新嵌套数据
if st.button("切换主题"):
    current = st.session_state.app_state["user"]["preferences"]["theme"]
    new_theme = "light" if current == "dark" else "dark"
    st.session_state.app_state["user"]["preferences"]["theme"] = new_theme
    st.rerun()

# 使用列表作为历史记录
if "search_history" not in st.session_state:
    st.session_state.search_history = []

search = st.text_input("搜索")
if search:
    st.session_state.search_history.append({
        "query": search,
        "time": datetime.now().isoformat()
    })

if st.session_state.search_history:
    st.write("搜索历史:")
    for item in reversed(st.session_state.search_history[-10:]):
        st.write(f"  - {item['query']} ({item['time'][:19]})")

2. 回调函数 ​

2.1 on_click 与 on_change ​

python
import streamlit as st

# 基本 on_click
if "count" not in st.session_state:
    st.session_state.count = 0

def increment():
    st.session_state.count += 1

def decrement():
    st.session_state.count -= 1

col1, col2, col3 = st.columns(3)
with col1:
    st.button("➕ 增加", on_click=increment)
with col2:
    st.write(f"当前值: {st.session_state.count}")
with col3:
    st.button("➖ 减少", on_click=decrement)

# on_change 回调
def on_name_change():
    st.toast(f"名字已更改为: {st.session_state.name_input}")

name = st.text_input(
    "输入名字",
    key="name_input",
    on_change=on_name_change
)

# selectbox 的 on_change
def on_option_change():
    st.toast(f"选择了: {st.session_state.option}")

option = st.selectbox(
    "选择选项",
    ["选项A", "选项B", "选项C"],
    key="option",
    on_change=on_option_change
)

# slider 的 on_change
def on_slider_change():
    st.session_state.slider_value = st.session_state.my_slider

slider_val = st.slider(
    "调节值",
    0, 100, 50,
    key="my_slider",
    on_change=on_slider_change
)

2.2 复杂回调模式 ​

python
import streamlit as st
import pandas as pd

# 待办事项应用
if "todos" not in st.session_state:
    st.session_state.todos = []

def add_todo():
    if st.session_state.new_todo.strip():
        st.session_state.todos.append({
            "text": st.session_state.new_todo.strip(),
            "done": False,
            "id": len(st.session_state.todos)
        })
        st.session_state.new_todo = ""

def remove_todo(index):
    st.session_state.todos.pop(index)

def toggle_todo(index):
    st.session_state.todos[index]["done"] = not st.session_state.todos[index]["done"]

def clear_completed():
    st.session_state.todos = [
        t for t in st.session_state.todos if not t["done"]
    ]

# 界面
st.title("待办事项")

with st.form("add_todo_form", clear_on_submit=True):
    new_todo = st.text_input("新任务", key="new_todo")
    submitted = st.form_submit_button("添加", on_click=add_todo)

# 显示待办
if st.session_state.todos:
    for i, todo in enumerate(st.session_state.todos):
        col1, col2, col3 = st.columns([0.5, 4, 0.5])
        with col1:
            st.checkbox(
                "",
                value=todo["done"],
                key=f"todo_{i}",
                on_change=toggle_todo,
                args=(i,)
            )
        with col2:
            text_style = "text-decoration: line-through; color: gray" if todo["done"] else ""
            st.markdown(f'<span style="{text_style}">{todo["text"]}</span>', unsafe_allow_html=True)
        with col3:
            st.button("🗑️", key=f"del_{i}", on_click=remove_todo, args=(i,))

    st.button("清除已完成", on_click=clear_completed)

    # 统计
    total = len(st.session_state.todos)
    done = sum(1 for t in st.session_state.todos if t["done"])
    st.progress(done / total if total > 0 else 0)
    st.write(f"完成 {done}/{total}")
else:
    st.info("暂无待办事项")

2.3 带参数的回调 ​

python
import streamlit as st

if "data" not in st.session_state:
    st.session_state.data = list(range(10))

def add_value(value):
    st.session_state.data.append(value)

def remove_value(index):
    st.session_state.data.pop(index)

# 批量添加按钮
for i in range(5):
    st.button(
        f"添加 {i}",
        key=f"add_{i}",
        on_click=add_value,
        args=(i,)
    )

# 显示数据
st.write("数据:", st.session_state.data)

# 删除按钮
for i, val in enumerate(st.session_state.data):
    st.button(
        f"删除 {val}",
        key=f"del_{i}",
        on_click=remove_value,
        args=(i,)
    )

3. Cache 缓存机制 ​

3.1 cache_data vs cache_resource ​

python
import streamlit as st
import pandas as pd
import numpy as np
from sqlalchemy import create_engine

# ========================
# st.cache_data - 缓存数据(推荐用于数据处理)
# ========================
# - 返回值会被深拷贝
# - 适用于返回 Pandas DataFrame、NumPy 数组、字典等可序列化数据
# - 每次调用返回独立副本,修改不会影响缓存

@st.cache_data
def load_data(file_path):
    """加载并处理数据(会被缓存)"""
    df = pd.read_csv(file_path)
    df["processed"] = df["value"] * 2
    return df

@st.cache_data(ttl=3600)  # 缓存1小时后过期
def fetch_api_data(url):
    """从 API 获取数据"""
    import requests
    response = requests.get(url)
    return pd.DataFrame(response.json())

@st.cache_data(show_spinner="正在加载数据...")
def expensive_computation(data):
    """耗时计算(会被缓存)"""
    result = data.groupby("category").agg({
        "value": ["mean", "sum", "count"]
    })
    return result

# ========================
# st.cache_resource - 缓存资源(推荐用于数据库连接等)
# ========================
# - 返回值不会被深拷贝
# - 适用于数据库连接、机器学习模型、文件句柄等不可序列化的对象
# - 全局共享,修改会影响所有用户

@st.cache_resource
def get_database_engine():
    """创建数据库连接(全局共享)"""
    return create_engine("sqlite:///mydb.db")

@st.cache_resource
def load_ml_model():
    """加载机器学习模型(全局共享)"""
    # import joblib
    # return joblib.load("model.pkl")
    return {"type": "model", "version": "1.0"}

# 使用
engine = get_database_engine()
model = load_ml_model()

3.2 缓存参数详解 ​

python
import streamlit as st
import pandas as pd
import time

# ttl:缓存过期时间(秒)
@st.cache_data(ttl=300)  # 5分钟过期
def get_fresh_data():
    return pd.DataFrame({"time": [time.time()]})

# max_entries:最大缓存条目数
@st.cache_data(max_entries=10)
def process_data(step):
    time.sleep(1)  # 模拟耗时操作
    return pd.DataFrame({"step": [step], "value": [step * 10]})

# show_spinner:加载提示
@st.cache_data(show_spinner="🔄 正在计算,请稍候...")
def slow_computation(n):
    time.sleep(2)
    return sum(i ** 2 for i in range(n))

# 实验性参数(Streamlit 1.37+)
@st.cache_data(
    ttl=3600,
    max_entries=100,
    show_spinner="加载中...",
    experimental_allow_widgets=True  # 允许缓存中包含 widget 值
)
def advanced_cache():
    return pd.DataFrame({"a": [1, 2, 3]})

# 使用示例
st.subheader("缓存演示")

if st.button("加载数据"):
    df = get_fresh_data()
    st.dataframe(df)

step = st.number_input("步骤", 1, 10, 1)
if st.button("处理"):
    result = process_data(step)
    st.dataframe(result)

n = st.number_input("计算 N", 1000, 1000000, 100000)
if st.button("计算"):
    result = slow_computation(n)
    st.write(f"结果: {result}")

3.3 缓存失效与清理 ​

python
import streamlit as st
import pandas as pd

# 缓存数据
@st.cache_data(ttl=3600)
def get_data():
    return pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

df = get_data()
st.dataframe(df)

# 清除特定函数的缓存
if st.button("清除数据缓存"):
    get_data.clear()  # 清除 get_data 的所有缓存
    st.success("缓存已清除!")
    st.rerun()

# 清除所有缓存
if st.button("清除所有缓存"):
    st.cache_data.clear()  # 清除所有 cache_data 缓存
    st.cache_resource.clear()  # 清除所有 cache_resource 缓存
    st.success("所有缓存已清除!")

# 也可以通过命令行清除
# streamlit cache clear

4. 多页面状态管理 ​

4.1 跨页面共享数据 ​

python
# utils/state_manager.py
"""跨页面状态管理工具"""
import streamlit as st

def init_global_state():
    """初始化全局状态"""
    defaults = {
        "user": {
            "name": "",
            "logged_in": False,
            "role": "guest"
        },
        "app": {
            "current_page": "home",
            "theme": "light",
            "language": "zh"
        },
        "data": {
            "uploaded_files": [],
            "current_dataset": None,
            "filters": {}
        }
    }
    for key, value in defaults.items():
        if key not in st.session_state:
            st.session_state[key] = value

def login_user(name, role="user"):
    """登录用户"""
    st.session_state.user.update({
        "name": name,
        "logged_in": True,
        "role": role
    })

def logout_user():
    """登出用户"""
    st.session_state.user.update({
        "name": "",
        "logged_in": False,
        "role": "guest"
    })

def get_current_user():
    """获取当前用户"""
    return st.session_state.get("user", {})

def is_logged_in():
    """检查是否已登录"""
    return st.session_state.get("user", {}).get("logged_in", False)
python
# app.py
import streamlit as st
from utils.state_manager import init_global_state, is_logged_in, get_current_user

st.set_page_config(page_title="多页面应用", layout="wide")
init_global_state()

# 导航
st.sidebar.title("导航")

if is_logged_in():
    user = get_current_user()
    st.sidebar.success(f"欢迎, {user['name']}!")
    if st.sidebar.button("登出"):
        from utils.state_manager import logout_user
        logout_user()
        st.rerun()
else:
    st.sidebar.info("请登录")

4.2 页面间数据传递 ​

python
# pages/1_数据上传.py
import streamlit as st
import pandas as pd

st.title("数据上传")

uploaded = st.file_uploader("上传CSV", type=["csv"])
if uploaded:
    df = pd.read_csv(uploaded)
    st.session_state.data = df
    st.session_state.data_info = {
        "filename": uploaded.name,
        "rows": len(df),
        "columns": list(df.columns)
    }
    st.success(f"已上传: {uploaded.name}")

# pages/2_数据分析.py
import streamlit as st

st.title("数据分析")

if "data" not in st.session_state or st.session_state.data is None:
    st.warning("请先在'数据上传'页面上传数据")
    st.stop()

df = st.session_state.data
info = st.session_state.get("data_info", {})

st.write(f"文件: {info.get('filename', '未知')}")
st.write(f"行数: {info.get('rows', 0)}")

st.dataframe(df.describe(), use_container_width=True)

# pages/3_数据可视化.py
import streamlit as st

st.title("数据可视化")

if "data" not in st.session_state or st.session_state.data is None:
    st.warning("请先在'数据上传'页面上传数据")
    st.stop()

df = st.session_state.data

numeric_cols = df.select_dtypes(include=["number"]).columns.tolist()
if numeric_cols:
    selected = st.selectbox("选择列", numeric_cols)
    st.line_chart(df[selected])
else:
    st.info("没有数值列可供可视化")

5. Fragment 局部刷新 ​

5.1 基本 Fragment ​

python
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime

st.title("Fragment 局部刷新示例")

# 全局数据(不会因 fragment 刷新而丢失)
if "global_counter" not in st.session_state:
    st.session_state.global_counter = 0

# Fragment 1:实时指标
@st.fragment(run_every="2s")
def live_metrics():
    st.subheader("实时指标")
    cols = st.columns(4)
    cols[0].metric("CPU", f"{np.random.randint(20, 80)}%")
    cols[1].metric("内存", f"{np.random.randint(40, 90)}%")
    cols[2].metric("磁盘", f"{np.random.randint(30, 70)}%")
    cols[3].metric("网络", f"{np.random.randint(100, 500)} Mbps")
    st.caption(f"更新时间: {datetime.now().strftime('%H:%M:%S')}")

# Fragment 2:带交互的图表
@st.fragment
def interactive_chart():
    st.subheader("交互式图表")
    chart_type = st.selectbox("图表类型", ["line", "bar", "area"], key="chart_type")
    data = pd.DataFrame({
        "x": range(20),
        "y": np.random.randn(20).cumsum()
    })

    if chart_type == "line":
        st.line_chart(data.set_index("x"))
    elif chart_type == "bar":
        st.bar_chart(data.set_index("x"))
    else:
        st.area_chart(data.set_index("x"))

# Fragment 3:局部表单
@st.fragment
def data_filter():
    st.subheader("数据筛选")
    min_val = st.slider("最小值", 0, 100, 0)
    max_val = st.slider("最大值", 0, 100, 100)

    # 返回筛选结果给主脚本
    return min_val, max_val

# 渲染
live_metrics()
st.divider()
interactive_chart()
st.divider()
min_val, max_val = data_filter()
st.write(f"筛选范围: {min_val} - {max_val}")

# 主脚本中的全局计数器(不受 fragment 影响)
if st.button("全局计数器 +1"):
    st.session_state.global_counter += 1
st.write(f"全局计数器: {st.session_state.global_counter}")

5.2 Fragment 间通信 ​

python
import streamlit as st
import pandas as pd
import numpy as np

st.title("Fragment 间通信")

if "shared_data" not in st.session_state:
    st.session_state.shared_data = pd.DataFrame({
        "category": ["A", "B", "C", "D"],
        "value": [10, 20, 15, 25]
    })

# Fragment A:数据编辑
@st.fragment
def data_editor():
    st.subheader("编辑数据")
    edited = st.data_editor(
        st.session_state.shared_data,
        num_rows="dynamic",
        use_container_width=True,
        key="editor"
    )
    if st.button("保存修改", key="save"):
        st.session_state.shared_data = edited
        st.success("已保存!")

# Fragment B:数据可视化
@st.fragment
def data_visualizer():
    st.subheader("数据可视化")
    if not st.session_state.shared_data.empty:
        st.bar_chart(st.session_state.shared_data.set_index("category"))
    else:
        st.info("暂无数据")

# Fragment C:统计信息
@st.fragment
def data_stats():
    st.subheader("统计信息")
    df = st.session_state.shared_data
    if not df.empty:
        st.metric("总和", df["value"].sum())
        st.metric("平均值", f"{df['value'].mean():.2f}")
        st.metric("最大值", df["value"].max())
    else:
        st.info("暂无数据")

# 渲染
col1, col2 = st.columns(2)
with col1:
    data_editor()
    data_stats()
with col2:
    data_visualizer()

6. 性能优化 ​

6.1 缓存最佳实践 ​

python
import streamlit as st
import pandas as pd
import numpy as np
import time

# ✅ 正确:缓存数据加载
@st.cache_data
def load_large_dataset():
    """加载大数据集 - 只在首次执行"""
    time.sleep(3)  # 模拟耗时加载
    return pd.DataFrame(
        np.random.randn(100000, 10),
        columns=[f"col_{i}" for i in range(10)]
    )

# ✅ 正确:缓存数据处理
@st.cache_data
def process_data(df, column, operation):
    """处理数据 - 根据参数缓存不同结果"""
    if operation == "mean":
        return df.groupby(column).mean() if column else df.mean()
    elif operation == "sum":
        return df.groupby(column).sum() if column else df.sum()
    elif operation == "count":
        return df.groupby(column).count() if column else df.count()

# ✅ 正确:缓存 ML 模型(全局共享)
@st.cache_resource
def load_model(model_name):
    """加载模型 - 全局共享,不会深拷贝"""
    time.sleep(2)  # 模拟模型加载
    return {"name": model_name, "version": "1.0"}

# ❌ 错误:不要在缓存函数中使用 st 组件
# @st.cache_data
# def bad_function():
#     st.write("这会报错")  # 不要在缓存函数中使用 st 组件
#     return data

# ✅ 正确:使用 show_spinner 提供用户反馈
@st.cache_data(show_spinner="正在加载数据,请稍候...")
def load_with_spinner():
    time.sleep(2)
    return pd.DataFrame({"a": [1, 2, 3]})

# 使用
df = load_large_dataset()
st.dataframe(df.head())

6.2 避免不必要的重运行 ​

python
import streamlit as st
import pandas as st
import time

# ❌ 错误:按钮点击后重运行整个脚本
# if st.button("加载数据"):
#     df = expensive_load()  # 每次点击都会重新执行

# ✅ 正确:使用 callback + session_state
if "data_loaded" not in st.session_state:
    st.session_state.data_loaded = False
    st.session_state.df = None

def load_data():
    time.sleep(2)  # 模拟耗时操作
    st.session_state.df = pd.DataFrame({"a": [1, 2, 3]})
    st.session_state.data_loaded = True

if st.button("加载数据", on_click=load_data):
    pass  # 实际工作在 callback 中

if st.session_state.data_loaded:
    st.dataframe(st.session_state.df)

# ✅ 正确:使用 st.empty 做局部更新
placeholder = st.empty()

if st.button("更新显示"):
    with placeholder.container():
        st.write("加载中...")
        time.sleep(1)
        st.write(f"更新完成: {time.time():.2f}")

# ✅ 正确:使用 fragment 做独立刷新
@st.fragment(run_every="5s")
def auto_refresh_section():
    st.write(f"自动刷新: {time.time():.2f}")

6.3 大数据集处理优化 ​

python
import streamlit as st
import pandas as pd
import numpy as np

@st.cache_data
def generate_large_data(n_rows):
    return pd.DataFrame({
        "id": range(n_rows),
        "value_a": np.random.randn(n_rows),
        "value_b": np.random.randn(n_rows),
        "category": np.random.choice(["A", "B", "C", "D"], n_rows),
    })

# 1. 分页显示
st.subheader("分页显示大数据集")
page_size = 100
total_rows = 10000

df = generate_large_data(total_rows)

page = st.number_input("页码", 1, total_rows // page_size, 1)
start = (page - 1) * page_size
end = start + page_size

st.dataframe(df.iloc[start:end], use_container_width=True)
st.write(f"显示第 {start+1}-{end} 行,共 {total_rows} 行")

# 2. 虚拟滚动(使用 st.dataframe 的 num_rows 参数)
st.subheader("虚拟滚动")
st.dataframe(df, height=400)  # 固定高度启用虚拟滚动

# 3. 采样显示
st.subheader("采样显示")
sample_size = st.slider("采样数量", 100, 5000, 1000)
sampled_df = df.sample(n=min(sample_size, len(df)), random_state=42)
st.dataframe(sampled_df, use_container_width=True)

6.4 内存优化 ​

python
import streamlit as st
import pandas as pd
import numpy as np

# 优化数据类型
@st.cache_data
def load_optimized_data():
    df = pd.DataFrame({
        "id": np.arange(1000000, dtype=np.int32),  # 使用 int32 而不是 int64
        "value": np.random.randn(1000000).astype(np.float32),  # 使用 float32
        "category": pd.Categorical(  # 使用 categorical 类型
            np.random.choice(["A", "B", "C", "D"], 1000000)
        ),
        "flag": np.random.choice([True, False], 1000000),  # bool 类型
    })
    return df

df = load_optimized_data()

# 显示内存使用
st.write(f"内存使用: {df.memory_usage(deep=True).sum() / 1024 / 1024:.2f} MB")
st.write(f"数据类型:\n{df.dtypes}")

# 只加载需要的列
st.subheader("选择列")
columns = st.multiselect("选择要显示的列", df.columns.tolist(), default=df.columns[:3].tolist())
if columns:
    st.dataframe(df[columns].head(100))

6.5 性能监控 ​

python
import streamlit as st
import time
import psutil
import os

def get_memory_usage():
    """获取当前进程内存使用"""
    process = psutil.Process(os.getpid())
    return process.memory_info().rss / 1024 / 1024  # MB

# 性能监控面板
@st.fragment(run_every="3s")
def performance_monitor():
    st.subheader("性能监控")
    col1, col2, col3 = st.columns(3)
    col1.metric("内存使用", f"{get_memory_usage():.1f} MB")
    col2.metric("CPU使用", f"{psutil.cpu_percent()}%")
    col3.metric("进程数", len(psutil.pids()))

performance_monitor()