news 2026/2/25 16:08:23

需要速度:Streamlit 与 Functool 缓存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
需要速度:Streamlit 与 Functool 缓存

原文:towardsdatascience.com/need-for-speed-streamlit-vs-functool-caching-eb3b7426f209

Streamlit 是我构建概念验证演示和分析仪表板的默认框架。该框架的简单性允许快速开发和易于维护。然而,简单的阴暗面是它内置了一些设计假设,这使得它难以作为顶级生产工具使用。我们将在稍后详细讨论这些假设,但这些假设的结果是 Streamlit 在处理和渲染你的应用程序时可能会非常慢。

在这篇文章中,我想向你展示两种提高你的 Streamlit 应用程序速度的方法:使用内置的Streamlit 缓存函数和使用内置的functools 缓存函数。这两种方法都基于缓存的概念,即如果某件事之前已经被触发,则输出会被保存以供以后重用。

在进入结果之前,我认为理解以下 3 个基本理论非常重要:Streamlit、Streamlit 缓存和 functools 缓存在底层是如何工作的。

PS:所有图片均由我创作,除非另有说明。

Streamlit 每次都会重新执行一切。每次。每次。

如介绍中所述,Streamlit 使用简单,但简单是有代价的。Streamlit 运行在一个独特的原则之上,使其与其他许多网络框架区别开来:每次用户与应用程序交互时,整个脚本都会从头到尾重新执行。就是这一切。这种行为可能看起来很奇怪,但它是 Streamlit 简单和强大的关键。

开发者设计 Streamlit 每次都重新执行的原因之一是为了使其默认情况下“无状态”。由于脚本每次都是完全重新执行,因此不需要显式地管理应用程序不同部分之间的状态。每次脚本运行都从一张白纸开始,所有内容都是根据当前输入重新计算的。

“无状态”很好,但想象一下,你有一个读取数据的函数。除非我们对此采取措施,否则 Streamlit 将会每次都重新运行读取数据的函数。脚本重新执行是我们遇到速度性能问题的原因。然而,有简单的方法可以解决这个问题。

理解 Streamlit 缓存

什么是 Streamlit 缓存?

Streamlit 的缓存机制允许你存储昂贵计算的结果,以便在后续脚本执行中重用。有了缓存,如果 Streamlit 知道一个函数或对象已经被调用过,它将跳过执行并立即返回缓存的“结果”,这可以显著加快你的应用速度。基本上,你打破了 Streamlit 的无状态执行模型,因为缓存允许应用的部分以有状态的方式运行,这样结果可以在脚本重新运行之间持久化。

Streamlit 提供了两个主要的缓存装饰器:

  1. @st.cache_data:这个装饰器非常适合缓存与数据相关的操作,例如加载数据集或查询数据库。这*“是所有返回数据的函数的默认命令 – 不论是 DataFrames、NumPy 数组、str、int、float 或其他可序列化类型。” (直接引用[3]中的内容)*

  2. @st.cache_resource:这个装饰器用于缓存资源,例如机器学习模型,其中资源需要初始化一次并多次重用。

如何调用 Streamlit 缓存

就像用**@st.cache_data**装饰你的函数一样简单。

@st.cache_data()deffiltering_pandas(df:pd.DataFrame,dates_filter=None,device_filter=None,ROI_filter=None,market_filter=None)->pd.DataFrame:# filtering operations...returndf

⚠️请注意!⚠️

在上面的函数中,我们有 5 个输入。Streamlit 会将这 5 个输入的任何组合视为一个新的缓存对象。例如,如果device_filter=Desktopdevice_filter=Mobile,Streamlit 将缓存两个不同的 dataframe 输出。你可以想象这会在内存大小方面如何爆炸。

设置约束以控制你的缓存内存使用

这里是 Streamlit 推荐的两个约束条件:

  1. ttl= 存活时间。其想法是强制 streamlit 在一段时间内使用缓存。“如果时间到了,你再次调用函数,应用将丢弃任何旧的缓存值,并重新运行该函数。”(直接引用[3]中的内容)*

  2. max_entries“设置缓存中的最大条目数。当向满缓存添加新条目时,最旧的条目将被移除。” (直接引用[3]中的内容)

如你所见,Streamlit 缓存非常容易使用。但是,了解 Streamlit 如何查看每个装饰过的函数,以确定你是否需要为你的应用设置约束条件,是非常重要的。现在让我们看看另一种缓存方式。

理解 functools 缓存

我从Fabian Bosler的帖子每个 Python 程序员都应该了解标准库中的 LRU_cache [1]中了解到functools.lru_cache但是,这仅涵盖了一个非常简单的示例,表明它非常快。然后我还发现了一篇由Marcin Kozak [2]写的帖子比较 functools 与 streamlit 缓存,但是,它只涵盖了 ETL 中的“数据读取”部分。我想尝试是否可以使它适用于更复杂的 ETL

functools缓存是什么?

functools.lru_cache是Python 的一个内置装饰器,它在存储函数调用结果到缓存方面与 Streamlit 缓存类似。如果函数再次以相同的参数(这里强调参数部分)被调用,Python 将返回缓存中的结果而不是重新计算它。

"LRU"代表最近最少使用,这是一种缓存策略,当缓存达到最大大小时,它会丢弃最不最近访问的项目。换句话说,它试图只保留内存中最频繁访问或最近的项目。这相当酷,因为你几乎可以“忘记”你本来需要在 Streamlit 中实现的约束控制(尽管你当然仍然可以控制缓存大小)

如何调用functools缓存

使用lru_cache就像用@lru_cache装饰你的函数一样简单。

@functools.lru_cache(maxsize=128)deffiltering_pandas(dates_filter=None,device_filter=None,ROI_filter=None,market_filter=None):# filtering operations...returndf

⚠️小心!⚠️

你是否注意到functools.lru_cache装饰的filtering_pandas()函数没有df()dataframe 作为输入?将此函数与st.cache_data()示例中使用的函数进行比较,你就会看到区别。原因是由于可哈希对象。

可哈希对象。如果你使用functools缓存,你将遭受的痛苦。

functools.lru_cache装饰器要求传递给缓存函数的所有参数都是可哈希的。一个 dataframe 是不可哈希的,因为它是可以变的。这是在编写带有functools.lru_cache装饰器的函数时遇到的一个痛点。

我将在稍后介绍我是如何处理这个问题的。

基准测试设计练习

在介绍了这两种缓存方法之后,是时候比较两者的性能了。为此,我创建了一个以下基准测试练习:

  1. 我创建了一系列合成 dataframe,行数从 1,000 到 10,000,000。

  2. 我创建了一个典型的 ETL 流程,其中我们加载数据,过滤它,将其与另一个 dataframe 连接,并根据一个段进行聚合。

  3. 这些 ETL 函数已经用以下方式编写:(1)Pandas (2)Polars (3)带有 Streamlit 缓存的缓存 pandas 函数 (4)带有 functools 缓存的缓存 pandas 和缓存 polars 函数。

  4. 我将这些封装到一个 Streamlit 应用程序中,其中我捕获了给定函数第一次运行和再次运行时的执行时间。

在进入结果部分之前,我想向您展示一些使用 Streamlit 和 functools 缓存的具体示例。

Streamlit 缓存 pandas 示例

下面你可以看到两个简单的 ETL 函数。它们都没有使用 streamlit 缓存装饰器,但我们调用了不同的函数。例如:

  • pandas_etl()调用了read_and_combine_csv_files()

  • 但是pandas_etl_streamlit_cached()调用了read_and_combine_csv_files_cached()

  • 实际上,你不需要创建两个不同的函数。我这样做是为了运行基准测试练习。

defpandas_etl(folder_path,secondary_df=None,dates_filter=None,device_filter=None,market_filter=None,ROI_filter=None,list_of_grp_by_fields=None,):df=read_and_combine_csv_files_pandas(folder_path)df=filtering_pandas(df=df,dates_filter=dates_filter,device_filter=device_filter,market_filter=market_filter,ROI_filter=ROI_filter)df=join_pandas(df,secondary_df)df=aggregating_pandas(df=df,list_of_grp_by_fields=list_of_grp_by_fields)returndf@st.cache_data()defpandas_etl_streamlit_cached(folder_path,secondary_df=None,dates_filter=None,device_filter=None,market_filter=None,ROI_filter=None,list_of_grp_by_fields=None,):df=read_and_combine_csv_files_pandas_cached(folder_path)df=filtering_pandas_cached(df=df,dates_filter=dates_filter,device_filter=device_filter,market_filter=market_filter,ROI_filter=ROI_filter)df=join_pandas_cached(df,secondary_df)df=aggregating_pandas_cached(df=df,list_of_grp_by_fields=list_of_grp_by_fields)returndf

在查看过滤函数时,这是我编写的代码:

  1. 构建基本的 pandas / python 过滤函数。因为我在为 Streamlit 编写函数,所以我拥有所有那些可选参数,这些参数将是用户输入的内容(如果没有输入,则默认为None)。

  2. 装饰一个调用非缓存函数的函数。如我所说,你不需要在你的最终应用程序中这样做,但我建议你以这种方式对缓存装饰器进行基准测试。

deffiltering_pandas(df:pd.DataFrame,dates_filter=None,device_filter=None,ROI_filter=None,market_filter=None)->pd.DataFrame:ifdates_filter:# Ensure the filter dates are datetime objectsdf['Date']=pd.to_datetime(df['Date'])start_date=pd.to_datetime(dates_filter[0])end_date=pd.to_datetime(dates_filter[1])df=df[(df['Date']>=start_date)&amp;(df['Date']<=end_date)]ifdevice_filter:df=df[df['Device'].isin(device_filter)]ifmarket_filter:df=df[df['Market'].isin(market_filter)]ifROI_filter:df=df[(df['ROI']>=ROI_filter[0])&amp;(df['ROI']<=ROI_filter[1])]returndf@st.cache_data()deffiltering_pandas_cached(df:pd.DataFrame,dates_filter,device_filter,ROI_filter,market_filter)->pd.DataFrame:returnfiltering_pandas(df,dates_filter,device_filter,ROI_filter,market_filter)

functools.lre_cache pandas or polars example

记得可哈希对象吗?这就是我处理使用functools.lru_cache的函数的方式。

  1. 如果你可以缓存一个不可变对象,就去做。例如,读取从 csv 文件中的数据集是不可变的。

  2. 如果你的函数尝试对数据框进行某些操作,你有两种选择:

    • 选项 A 是对数据框进行哈希,并将其作为输入参数传递。这可能会降低你的速度性能,因为你在对数据框进行哈希,但你可以在 Streamlit session_state 变量中保存这个哈希对象以供以后重用。

    • 选项 B 是简单地调用缓存函数,你的数据是从那里读取的,并运行整个 ETL。这不太符合编程标准,但如果工作足够小,我不认为这有什么问题。

  3. 如果你的函数需要其他输入,请将这些输入设置为不可变。这与选项 A 相同,如果你在处理数据框时。例如,列表是可变的,所以 functools 不喜欢它。但是,如果你将列表转换为一个元组,那么它将视为一个不可变对象(你也可以对其进行哈希,但这是一种过度行为)。

查看下面的示例:

@functools.lru_cachedefread_and_combine_csv_files_pandas_cached_functools(folder_path):returnpd.read_csv(folder_path)@functools.lru_cachedefpandas_functools_etl(folder_path,dates_filter=None,device_filter=None,market_filter=None,ROI_filter=None,list_of_grp_by_fields=None):# This is the equivalent of option B, where I call the read function.# The read function is also cached, so effectively, this line will# be faster after the first run.df=read_and_combine_csv_files_pandas_cached_functools(folder_path)# All the filters and aggregation fields below look like lists right?# See the how are we using the pandas_functools_etl() at the endifdates_filter:# Ensure the filter dates are datetime objectsdf['Date']=pd.to_datetime(df['Date'])start_date=pd.to_datetime(dates_filter[0])end_date=pd.to_datetime(dates_filter[1])df=df[(df['Date']>=start_date)&amp;(df['Date']<=end_date)]ifdevice_filter:df=df[df['Device'].isin(device_filter)]ifmarket_filter:df=df[df['Market'].isin(market_filter)]ifROI_filter:df=df[(df['ROI']>=ROI_filter[0])&amp;(df['ROI']<=ROI_filter[1])]markets_pandas_df=pd.read_csv('synthetic_data/data_csv/dataset_markets/markets.csv')df=pd.merge(df,markets_pandas_df,on='Market',how='inner')iflist_of_grp_by_fields:df=(df.groupby(list(list_of_grp_by_fields)).agg({**{field:'sum'forfieldinsum_fields},**{field:'mean'forfieldinmean_fields}}))# Rename columns to clarify which operation was performeddf.columns=[f'{col}_{"Sum"ifcolinsum_fieldselse"Avg"}'forcolindf.columns]df=df.reset_index()returndf# We create inmutable objects from lists by creating tuplesimmutable_device_filter=tuple(device_filter)ifdevice_filterelseNoneimmutable_market_filter=tuple(market_filter)ifmarket_filterelseNoneimmutable_ROI_filter=tuple(ROI_filter)ifROI_filterelseNoneimmutable_list_of_grp_by_fields=tuple(list_of_grp_by_fields)iflist_of_grp_by_fieldselseNonepandas_functools_cached_df=pandas_functools_etl(folder_path=folder_path,dates_filter=dates_filter,device_filter=immutable_device_filter,market_filter=immutable_market_filter,ROI_filter=immutable_ROI_filter,list_of_grp_by_fields=immutable_list_of_grp_by_fields,)

上述代码就是这样工作的(尽管你可以通过注释来跟踪它):

  1. 读取数据函数被缓存。任何时候,任何调用此读取方法的函数,缓存都会启动,并且不需要进行读取。

  2. 上述示例遵循选项 B,其中我没有对数据框进行哈希并添加为输入参数,而是直接调用了读取方法。

  3. 最后,尽管函数的输入以及 pandas 使用如 _marketfilter等参数的方式使其看起来像列表,但我们实际上传递的是一个包含列表的元组。这样我们就“欺骗”了缓存操作,让它认为它是一个不可变对象。

基准测试结果

而现在就是真相大白的时候… 🥁🥁🥁

第一次运行

顺便说一下,绝对执行时间本身并不太重要(取决于你的机器等),但了解我们正在处理哪些基线是很好的。

下面的图表显示了使用指定行数的 ETL 在每个选项下第一次运行所需的时间。在这里,缓存还没有启动。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/aed7776bdfc40997aa9bf51efc12cae1.png

第一次运行的执行速度。

你可以看到:

  1. 对于较小的数据集,polars 与 pandas 之间没有区别(所有框架都少于 1 秒)。但对于较大的数据集,仅使用 polars 就能大幅提高效率(见 10m,其中 pandas 需要 8 秒,而 polars 大约 1 秒)。我们已知 polars 比 pandas 快,但嘿,总是好事。

  2. 如果我们关注较大的数据集,你可以观察到两种缓存方法都会给非缓存方法增加一点延迟。例如,对于 10m 行数据集:

    • pandas 运行 ETL 需要 8 秒

    • functools 需要 9.5 秒;比 pandas 非缓存版本慢 16%

    • streamlit 需要 10.5 秒;_ 比 pandas 非缓存版本慢 24%_ 这是因为你的机器在第一次看到这些内容时需要一些时间来缓存它们。

第二次运行

对于第二次运行,我们将关注更大的数据集。正如所见,对于较小的数据集,你可能甚至不需要担心所有这些缓存操作。

当我第二次重新运行整个操作时,在速度性能方面结果绝对令人难以置信。让我们分别比较 pandas 和 polars 的速度提升,因为我们感兴趣的是基于缓存的性能提升,而不是 polars 与 pandas 的比较。

Pandas

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c423b5f450c33f099596e5ea96493cfa.png

pandas ETL 的速度提升

Polars

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/74a0fcdc52f394be604cff5a2845fe5d.png

polars ETL 的速度提升

太不可思议了。Functools 将性能提升到了几乎 0 秒!!这太神奇了!让我们看看框架打印出的确切数字。

相对执行时间改进

在下面的表格中,你可以看到输出打印的分解:

---------------------------------------------------------------------------synthetic_data/data_csv/dataset_10000000---------------------------------------------------------------------------Pandas read data:5.683701038360596Pandasfilter:1.9549269676208496Pandas join:0.7461288452148438Pandas aggregation:0.7051977062225342Pandas ETL execution timeinseconds:9.097726821899414Pandas Streamlit Cached ETL execution timeinseconds:2.2573580741882324Pandas functools cached ETL execution timeinseconds:4.0531158447265625e-06

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d50bb825e6ce9b5dd13d02b5f4693b25.png

执行速度的视觉展示

对于 polars,我们看到同样的情况:

Polars read data:0.41807007789611816Polarsfilter:0.10534787178039551Polars join:0.3411428928375244Polars aggregation:0.10782408714294434Polars ETL execution timeinseconds:0.9924719333648682Polars functools cached ETL execution timeinseconds:5.9604644775390625e-06

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9b27117c56aad89c9f16f8c14578915a.png

执行速度的视觉展示


摘要

在这篇文章中,我们:

  • 讨论了 Streamlit、Streamlit 缓存和 functools 缓存在底层是如何工作的理论。

  • 此外,我还提供了一些关于如何编写 Streamlit 缓存和 functools 缓存的示例。

  • 我们最终进行了一次基准测试,并看到了与不缓存相比,缓存是多么地快。特别是对于 functools。

TLDR 版本如下:

Streamlit 缓存编写起来更容易,但如果您需要巨大的改进收益,或者您正在使用 polars,那么 functools 缓存应该是您的首选选项。

PS:Streamlit 缓存与 polars 不兼容。

代码在哪里可以找到?

在我的 GitHub 仓库中:github.com/JoseParrenoGarcia/Streamlit-pretty-dataframes

致谢

  • [1] Fabian Bosler 发表的帖子 Every Python Programmer Should Know LRU_cache From the Standard Library,让我发现了 Python 的 functools 缓存。

  • [2] Marcin Kozak 发表的帖子 Improved Caching Produces a 5000x Performance Boost on Streamlit Dashboards,用于基准测试“读取数据”的性能。

  • [3] Streamlit 官方文档关于缓存的说明

进一步阅读

感谢阅读这篇文章!如果您对我的其他书面内容感兴趣,这里有一篇文章收集了我所有其他博客文章,按主题组织:数据科学团队和项目管理、数据故事讲述、营销与出价科学以及机器学习与建模。

所有我的书面文章都在一个地方

请保持关注!

如果您想在我发布新书面内容时获得通知,请随意在 Medium 上关注我或订阅我的 Substack 通讯。此外,我很乐意在 LinkedIn 上与您聊天!

获取关于数据科学的最新书面内容通知

Jose 的 Substack | Jose Parreño Garcia | Substack

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/18 8:06:19

PCL2-CE社区版:打造你的专属Minecraft启动器终极指南

PCL2-CE社区版&#xff1a;打造你的专属Minecraft启动器终极指南 【免费下载链接】PCL2-CE PCL2 社区版&#xff0c;可体验上游暂未合并的功能 项目地址: https://gitcode.com/gh_mirrors/pc/PCL2-CE PCL2-CE社区版作为一款功能强大的Minecraft启动器&#xff0c;不仅提…

作者头像 李华
网站建设 2026/2/24 2:25:42

Unity实时翻译工具:XUnity.AutoTranslator全攻略

Unity实时翻译工具&#xff1a;XUnity.AutoTranslator全攻略 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator是一款专为Unity引擎游戏设计的实时翻译工具&#xff0c;能够实时转换游…

作者头像 李华
网站建设 2026/2/24 23:20:19

Godot Unpacker高效资源提取工具配置与应用指南

Godot Unpacker高效资源提取工具配置与应用指南 【免费下载链接】godot-unpacker godot .pck unpacker 项目地址: https://gitcode.com/gh_mirrors/go/godot-unpacker Godot Unpacker是一款专为Godot游戏引擎设计的高效资源提取工具&#xff0c;能够帮助开发者和游戏爱好…

作者头像 李华