Streamlit 论坛上最常见的 10 个解释

Streamlit 初学者指南

发布于 倡导者帖子
10 most common explanations on the Streamlit forum

Streamlit 初学者指南

嗨,社区!👋

我叫 Debbie Matthews,是Streamlit 出色论坛的版主。你可能见过我的 ID mathcatsand,意思是“数学、猫和……”。

如果你在论坛里呆得够久,你会开始看到一些常见的问题和令人困惑的地方。我认为对于新用户来说,了解许多人在刚开始使用 Streamlit 时会遇到的障碍会很有帮助。

在这篇帖子中,我将谈论其中的 10 个。

  1. 按钮不是有状态的。
  2. Streamlit 的渲染方式与终端不同。
  3. 你可以注入自己的 CSS 和 JavaScript。
  4. 你目录中的文件不会隐式地供前端访问。
  5. file_uploader 不会将文件保存到你的目录中。
  6. 会话状态中的键在与其关联的 widget 未渲染时会消失。
  7. 你的本地环境与你的云环境不同。
  8. Streamlit 本身不具备此功能,但是…
  9. 这不是 Streamlit 的问题。
  10. 你能否提供更多信息?
💡
此帖子的代码片段在此实时应用中托管,所以请随意打开另一个选项卡或窗口以便跟随。如果你还没有阅读或观看关于 Streamlit 入门的介绍,请在此查看。 

1. 按钮不是有状态的

按钮仅在其被点击后页面加载时返回 True,并立即变回 False。

如果你创建一个 if 语句来检查按钮的值,if 语句的主体会在按钮每次点击时执行一次。这里应该包含的是简短的消息或你不想随其他用户活动一起重新运行的进程。

import streamlit as st

if st.button('Submit'):
    st.write('Submitted!')

if st.button('Confirm'):
    st.write('Confirmed!')

如果你嵌套按钮,代码的最内层部分将永远不会执行!一旦你点击第二个按钮,页面将重新加载,而第一个按钮的值将是 False

import streamlit as st

if st.button('First Button'):
    st.write('The first button was clicked.')
    if st.button('Second Button'):
        # This will never execute!
        st.write('The second button was clicked')

如果你需要按钮的行为更像复选框,你可以在会话状态中创建一个键来保存该信息。

import streamlit as st

# Initialize the key in session state
if 'clicked' not in st.session_state:
    st.session_state.clicked = {1:False,2:False}

# Function to update the value in session state
def clicked(button):
    st.session_state.clicked[button] = True

# Button with callback function
st.button('First Button', on_click=clicked, args=[1])

# Conditional based on value in session state, not the output
if st.session_state.clicked[1]:
    st.write('The first button was clicked.')
    st.button('Second Button', on_click=clicked, args=[2])
    if st.session_state.clicked[2]:
        st.write('The second button was clicked')
💡
请查看 streamlit-extras,它是由各种贡献者提供的许多有用的自定义组件的集合。它包含由 Zachary Blackwood 制作的有状态按钮。

2. Streamlit 的渲染方式与终端不同

每次页面交互时,Streamlit 都会重新加载页面。它并不是用来等待输入然后继续执行的。它也不会保留屏幕上未明确重新渲染的任何内容。

谨慎使用循环和条件语句!你不会希望 while 循环等待用户输入。Streamlit 不会因为新的 widget 而暂停等待输入;它只是继续使用其默认值。如果你的循环中有 widget,Streamlit 会尝试在每次循环通过时创建一个新的、额外的 widget。如果你需要等待用户的选择,请在输出上设置一个条件语句来检查其是否具有默认值。

import streamlit as st

name = st.text_input('Name:')
if name != '':
    st.write(f'Hi, {name}! Nice to meet you.')

如果你需要确认用户的选择(可能是默认值),你可以添加一个确认按钮。你可以要求对任何选择进行确认,或仅作为用户接受默认值的一种方式。

import streamlit as st

# Create a key in session state to record the user's choice, defaulting to None
if 'favorite_color' not in st.session_state:
    st.session_state.favorite_color = None

# Confirmation function to record the user's choice into the favorite_color key
def confirm_color():
    st.session_state.favorite_color = st.session_state.color_picker

name = st.text_input('Name:')
if name != '':
    st.write(f'Hi, {name}! Nice to meet you.')
    st.write(f'What\\'s your favorite color?')
    # Confirmation function will run if the user changes the widget
    color = st.color_picker('Color:', key='color_picker', on_change=confirm_color)
    if st.session_state.favorite_color is None:
        # Or, Confirmation function will run if user confirms the default
        st.button('Confirm Black', on_click=confirm_color)
    else:
        st.write(f'<span style="color:{color}">Oh, nice color choice!</span>', 
            unsafe_allow_html=True)

如果你有许多要显示的交互步骤,嵌套的 if 语句可能会变得难以控制。你可以改为在会话状态中创建一个暂存值来控制页面上显示的内容量。你可以使用如下例所示的不等式来显示所有先前的阶段。或者,你可以使用相等或 elif 来仅显示当前阶段。

import streamlit as st

# Create a key in session state to track the stage
if 'stage' not in st.session_state:
    st.session_state.stage = 0

# Stage function to update the stage saved in session state
def set_stage(stage):
    st.session_state.stage = stage

st.write('Welcome! Click to begin.')
# Each button runs the Stage function, passing the stage number as an argument
st.button('Begin', on_click=set_stage, args=[1])

# Content for each stage within the body of an if statement
if st.session_state.stage > 0:
    st.write('This is stage 1. Do some things.') 
    st.button('Next', on_click=set_stage, args=[2])
if st.session_state.stage > 1:
    st.write('This is stage 2. Do some more things.')
    st.button('Finish', on_click=set_stage, args=[3])
if st.session_state.stage > 2:
    st.write('This is the end. Thank you!')
    st.button('Reset', on_click=set_stage, args=[0])

如果你想要一个函数,每次点击都能“添加数据”,你需要一个在会话状态中累积这些添加项的东西。这通常通过在脚本顶部使用 if 'key' not in st.session_state: 来实现。这样,只有在第一次加载页面时才会初始化“新”的、未修改的对象。每次添加时,对象都不会被其默认值覆盖,因为键已经存在。

import streamlit as st
import pandas as pd

# Initialize some object in session state where you will you be storing edits
if 'df' not in st.session_state:
    st.session_state.df = pd.DataFrame({'A':[1,2,3],'B':[4,5,6],'C':[7,8,9]})

# Optional: Assign the stored value to a convenient variable for brevity in code
df = st.session_state.df

st.dataframe(df)

cols = st.columns(3)
cols[0].number_input('A',0,100,step=1, key='A')
cols[1].number_input('B',0,100,step=1, key='B')
cols[2].number_input('C',0,100,step=1, key='C')

def add_row():
    row = [st.session_state.A, st.session_state.B, st.session_state.C]
    next_row = len(st.session_state.df)
    # Make sure modifcation is performed on the object in session state
    st.session_state.df.loc[next_row] = row

st.button('Add Row', on_click=add_row)

3. 你可以注入自己的 CSS 和 JavaScript

HTML 和 CSS 可以通过 st.writest.markdown 添加(使用正确的可选关键字)。JavaScript 需要更健壮的 components 子模块。

许多不同的资源描述了修改 Streamlit 显示方式的方法。另一位 Streamlit Creator Fanilo Andrianasolo 有一个解释基础知识的简短视频。这里有一些例子。

想改变按钮的字体颜色,包括悬停和焦点颜色?这里是如何做到的

import streamlit as st

st.button('Click me!')

css='''
<style>
    .stButton > button {
        color: red;
    }
    .stButton > button:hover {
        color: violet;
        border-color: violet;
    }
    .stButton > button:focus {
        color: purple !important;
        border-color: purple !important;
        box-shadow: purple 0 0 0 .2rem;
    }
</style>
'''

st.markdown(css, unsafe_allow_html=True)

请注意在使用 st.markdownst.write 时使用 unsafe_allow_html=True。需要这个可选关键字来阻止 Streamlit 转义 HTML 标签。如果你了解你的 CSS 选择器,你可以获取任何元素。我经常使用一组容器结合 nth-of-type 选择器来获取元素的特定实例。

import streamlit as st

# Layout your containers at the beginning
section1 = st.container()
section2 = st.container()
section3 = st.container()
section4 = st.container()

# Write to the different containers for your display elements
section1.subheader('Section 1')
section1.button('Button 1')

section2.subheader('Section 2')
section2.button('Button 2')

section3.subheader('Section 3')
section3.button('Button 3')

section4.subheader('Section 4')
section4.button('Button 4')

css='''
<style>
    section.main > div > div > div > div:nth-of-type(3) .stButton > button {
        color: green;
    }
    section.main > div > div > div > div:nth-of-type(3) .stButton > button:hover {
        color: violet;
        border-color: violet;
    }
    section.main > div > div > div > div:nth-of-type(3) .stButton > button:focus {
        color: purple !important;
        border-color: purple !important;
        box-shadow: purple 0 0 0 .2rem;
    }
</style>
'''

st.markdown(css, unsafe_allow_html=True)

如果你需要自定义纯 CSS 无法处理的东西,请使用 components 子模块。当你插入一个组件时,它会包含在一个 iframe 中。请注意,你的 JavaScript 查询必须触及该 iframe 之外才能按预期工作。

import streamlit as st

st.header('Screen Width Checker')
st.write('''<h3>The app container is <span id="root-width"></span> x 
<span id="root-height"></span> px.</h3>
''', unsafe_allow_html=True)

js = '''
<script>
    var container = window.parent.document.getElementById("root")

    var width = window.parent.document.getElementById("root-width")
    var height = window.parent.document.getElementById("root-height")

    function update_sizing(){
        width.textContent = container.getBoundingClientRect()['width']
        height.textContent = window.parent.innerHeight
    }
    update_sizing()

    window.parent.addEventListener('resize', function(event) {
        update_sizing()
    }, true);
    
</script>
'''

st.components.v1.html(js)

4. 你目录中的文件不会隐式地供前端访问

用户无法直接从你的应用服务器上的文件中选择。你无法像在 Web 主机上那样访问文件。

Streamlit 采用服务器-客户端结构。用户可以访问的文件位于他们打开浏览器的计算机上。Streamlit 只会允许用户访问你明确指示其提供的文件。如果你的工作目录中保存了图像 my_image.png,则该图像无法通过 <app url>/my_image.png 访问。

当你在应用程序中使用 st.image 时,Streamlit 将创建一份数据的副本,并通过一个哈希文件名使其可供客户端的浏览器访问。当你在应用程序中使用包含文件路径的 HTML 或 CSS 时,你需要将该文件托管在某个地方。文件不会仅仅因为它位于你的应用程序目录中就能通过网络访问。

对于 HTML 和 CSS,你可以打开并读取文件的内容,然后手动注入其内容。CSS 文件的内容不应包含指向其他 HTML、CSS 或图像文件的相对路径,因为这些文件将无法供用户的客户端访问。

import streamlit as st

if 'css' not in st.session_state:
    with open('files/my_css.css', 'r') as file:
        css = file.read()
    st.session_state.css = css

css = '<style>' + st.session_state.css + '</style>'

st.button('Click me!')

st.markdown(css, unsafe_allow_html=True)

Streamlit 1.18.0 中还有一个新的、令人兴奋的功能:静态文件!如果你想让工作目录中的任何内容可通过网络访问,你也可以使用此功能。假设你有一个你想在某些 CSS 中指定的背景图像。如果你开启静态托管并将背景图像放在名为 static 的文件夹中,你可以在 CSS 中使用它。请务必阅读链接文档以 clarification。

import streamlit as st

image = './app/static/cat_background.jpg'

css = f'''
<style>
    .stApp {{
        background-image: url({image});
    }}
    .stApp > header {{
        background-color: transparent;
    }}
</style>
'''
st.markdown(css, unsafe_allow_html=True)

你的 config.toml 应包含

[server]
enableStaticServing = true

请记住在每次更改环境或配置后重新启动你的应用程序!在此处阅读更多关于配置的信息。

5. file_uploader 不会将文件保存到你的目录中

file_uploader widget 返回一个“类文件对象”,即文件的数据。此对象无法通过名称或路径访问。

你可能熟悉 file_uploader 的典型用例:

import streamlit as st
import pandas as pd

file = st.file_uploader("Choose a file:", key="loader", type='csv')

if file != None:
    df = pd.read_csv(file)
    st.write(df)

由于通过路径指定数据文件给 read_csv 非常常见,很容易忘记 pandas 接受路径或缓冲区。在上面的例子中,我们传递的是后者。变量 file 没有与其关联的“路径”。你可以通过继承自 BytesIO 的 name 属性访问文件的名称,但这仅用于信息目的。你不用文件的名称来指向其数据。有许多库和函数不接受类文件对象而不是路径。请注意你正在使用的函数,如有疑问,请始终阅读其文档。

另外请注意,你获取的文件状对象需要经过处理才能被解释,即使它是一个简单的文本文件。

import streamlit as st
import io

file = st.file_uploader("Choose a file:", type=['css','py'])

if file != None:
    bytes_object = file.getvalue()
    string_object = bytes_object.decode("utf-8")

    st.code(string_object)

6. 会话状态中的键在与其关联的 widget 未渲染时会消失

当会话状态中的键与 widget 关联时,当 widget 不再渲染时,该键将从会话状态中移除。如果你导航到不同的页面或在同一页面上有条件地渲染 widget,可能会发生这种情况。

以下是 widget 生命周期 的简要描述。

在你首次调用 widget 的特定行,Streamlit 会创建一个新的前端 widget 实例。如果你指定了键,Streamlit 会检查该键是否已存在于会话状态中。如果该键不存在,Streamlit 会创建一个,并从 widget 的默认值开始。但是,如果 Streamlit 看到该键已存在,它会将其连接到该 widget。在这种情况下,即使是具有指定初始值的新 widget,也会采用该键的值。

示例:此滑块的值将始终为 1,因为 widget 总是会连接到预先存在的键。

import streamlit as st

st.session_state.my_key = 1

st.slider('Test', 0, 10, key='my_key')

尽管你可以通过将会话状态中的不同值分配给 widget 的键来编辑其状态,但会话状态只是一个中介。 widget 在屏幕上持续渲染时会拥有并保留状态,即使你从会话状态中删除其键。

例如,此 widget 现在并将保持有状态:

import streamlit as st

st.session_state.clear()

st.slider('Test', 0, 10, key='my_key')

然而,一旦 widget 不被渲染(即使只是加载一次页面),Streamlit 会删除其所有数据,包括会话状态中与其关联的任何键。

import streamlit as st

switch = st.radio('Choice:', [1,2])

match switch:
    case 1:
        st.checkbox('1', key='1')
    case 2:
        st.checkbox('2', key='2')
  1. 在上面的例子中,假设单选按钮选择了 1。在这种情况下,会话状态中会有一个 '1',复制了 widget 的状态。
  2. 一旦用户为单选按钮选择 2,页面将重新加载。当 Streamlit 重新运行页面时,会话状态中仍然会有 '1' 键。它不知道该键关联的 widget 将不会被渲染。
  3. 然而,一旦 Streamlit 完成页面渲染,它会发现它拥有一个未渲染的 widget 的信息。此时,Streamlit 将删除 widget 信息,包括任何与其关联的会话状态中的键。

此清理过程对于条件渲染的 widget 特别重要。对于旨在传递到其他页面(通常在侧边栏中)的 widget 也同样重要。正在讨论改变此行为,并可能更深层次地改变结构。目前,请记住当一个键分配给一个 widget 时,如果你离开该 widget 的实例,会话中的数据将被删除。

两种解决方法:

  1. 将数据复制到会话状态中的新键中,以便在会话状态中有一个不受意外删除的位置。
  2. 在页面顶部将数据重新提交到会话状态。通过在每个页面顶部使用 st.session_state.my_key = st.session_state.my_key,你可以人为地“保持它活着”。当从带有 key='my_key' 的 widget 导航离开时,这会中断清理过程。此手动值分配实际上将键与 widget 分离(直到再次看到带有该键的 widget)。

7. 你的本地环境与你的云环境不同

确保为任何部署指定/使用正确的环境,并确保你的文件路径是独立于操作系统的。

当你将应用程序部署到某个云服务时,该云服务内的一个新的 Python 环境将被用来运行你的应用程序。它不会知道或使用你的本地环境中碰巧拥有的任何东西。你必须告诉你的云环境所有需要安装的 Python 包以及任何额外的非 Python 组件。

对于 Streamlit Cloud,最常见的方法是在工作目录的顶部保存一个 requirements.txt 文件。requirements.txt 文件中的每一行都指定了一个要在云环境中 pip install 的包。你也可以通过这种方式设置特定版本的 Python 包。

requirements.txt 文件示例:

streamlit==1.17.0
pandas
numpy

一些 Python 库需要安装额外的命令行工具或软件。Streamlit Cloud 是基于 Debian 的 Linux 容器。额外的软件通过 apt-get 安装,方式类似于 pip 安装 Python 包。你需要在工作目录中 alongside 你的主 Python 文件,以便应用程序使用一个 packages.txt 文件。packages.txt 文件中的每一行都指定了一个要在云环境中 sudo apt-get install 的二进制文件。

packages.txt 文件示例:

ffmpeg
chromium
chromium-chromedriver

由于许多人的本地环境与 Linux 不同,请注意 Linux 是区分大小写的。指定你的环境的文件必须完全按照规定命名,包括大小写。在 Python 脚本中书写文件路径时,使用正斜杠而不是反斜杠。确保所有路径都从工作目录提供,即使是多页应用程序的 pages 文件夹中的 Python 文件。

正如文档中所述,还有其他方法可以指定你的 Python 包。例如,你可以有一个 environment.yml 文件来使用 conda 而不是使用 requiremnts.txt 来使用 pip。如果你尝试同时包含两者,Streamlit Cloud 只会处理它遇到的第一个,并忽略第二个。

对于在 Streamlit Cloud 上的部署,这里有一个相关的警告。如果你在脚本中写入文件,该更新的文件只存在于托管你应用程序的 Debian 容器中。它不会保存回 GitHub,也不会在应用程序重新启动后存活。

8. Streamlit 本身不具备此功能,但是…

Streamlit 在不断发展和改进,因此请密切关注路线图,并注意最常用的自定义组件。

有几个不错的地方可以让你了解即将推出的内容,帮助你了解我们目前处于什么阶段。密切关注路线图,了解正在开发中的内容。 GitHub Issues 是开发者跟踪功能请求的官方场所。如果你想让 Streamlit 实现新功能,请先在那里查看,以便你可以为你遇到的任何现有请求点赞,或者如果还没有人提及,就创建一个新的请求。我喜欢按点赞数排序列表,看看哪些内容正在获得关注。

查看Streamlit 组件社区跟踪器,了解人们构建的额外功能。以下是一些 noteworthy 的包:

💡
如果你的应用程序托管在与用户计算机(客户端)不同的计算机(服务器)上,请注意计算机外围设备。使用 Streamlit 兼容的库。因此,有许多组件可以处理音频/视频输入。
💡
请注意,Streamlit 1.18.0 也引入了实验性的可编辑数据框架。

9. 这不是 Streamlit 的问题

如果出现问题的代码行中不包含 Streamlit 库的任何内容,请仔细思考。问问自己你需要 Streamlit 的帮助还是其他方面的帮助。

社区成员非常慷慨地帮助解决非 Streamlit 问题。但是,最好将你的问题导向正确的渠道。互联网上有很多有用的论坛,关注的领域各不相同。在专注于你问题的论坛中提问,你会得到最好、最快的帮助。

Streamlit 唯一做的事情就是为你的 Python 代码提供一个前端。如果你在从工作目录中的 CSV 文件创建数据框时遇到问题,你可能有一个关于 pandas 的问题,而不是 Streamlit 的问题。获得答案的最有效途径是寻求专门针对 pandas 的论坛。

当你遇到代码行上的困难时,检查是否涉及任何 Streamlit 组件。如果没有,我建议你尝试在不使用 Streamlit 的情况下执行该段代码。如果合适,你可以创建一个并运行一个普通的 Python 脚本,或者在 Jupyter notebook 中尝试。如果在 Jupyter notebook 中工作正常,但在 Streamlit 中的行为与你预期不符,那么这是向 Streamlit 论坛提出的绝佳问题。

10. 你能否提供更多信息?

如果你投入时间清晰简洁地提出问题,你可能会节省同样多甚至更多等待回复的时间。

社区成员越容易理解你的问题,你就会越快得到回复。如果你在部署时遇到问题,我们希望看到你的 GitHub 仓库。我们想了解你如何配置环境并检查是否有拼写错误。我们想看到重新启动后的终端输出,以查看是否有任何错误消息。另一方面,如果前端显示方式与你预期不同,我们想知道你正在使用的代码以及你实际看到的屏幕截图。请解释你期望的样子。

代码的屏幕截图帮助不大,因为我们无法将它们复制粘贴到可工作的代码片段中。访问你的完整 GitHub 仓库可能很有用,有时也是必要的。然而,重现问题所需的最少量代码总是最好的。如果我们可以复制粘贴你提供的代码片段并启动它以查看你的问题,那就完美了!请包含你的 import 语句以及你的脚本访问的任何文件。提供简化的、虚拟的数据供我们使用。内联数据最容易处理,例如在代码片段中定义一个简单的数据框。如果导入数据是问题的一部分,我们将需要一个示例数据文件来配合你的代码片段。

如果你花时间创建一个小而独立的示例来描述你的问题,社区就可以帮助你。否则,我们会花费大量时间检查你的代码、伪造数据并进行各种猜测来填补空白。

请查看此指南,了解如何在论坛上有效发布帖子。我特别想提请你注意最小可复现示例的概念。

总结

感谢阅读!我希望你找到了一些有用的信息,在你开始使用 Streamlit 时为你节省时间和麻烦。如果你有任何问题,请在下面的评论中发布,或在Streamlit 论坛上联系我。你也可以在GitHubLinkedIn上找到我。

Streamlit-ing 快乐!🎈

分享此帖子

评论

在我们的论坛中继续对话 →

也在倡导者帖子中...

查看更多 →