需要速度:Streamlit 与 Functool 缓存

📅 发布时间:2026/7/5 15:06:03 👁️ 浏览次数:
需要速度:Streamlit 与 Functool 缓存
原文towardsdatascience.com/need-for-speed-streamlit-vs-functool-caching-eb3b7426f209Streamlit 是我构建概念验证演示和分析仪表板的默认框架。该框架的简单性允许快速开发和易于维护。然而简单的阴暗面是它内置了一些设计假设这使得它难以作为顶级生产工具使用。我们将在稍后详细讨论这些假设但这些假设的结果是 Streamlit 在处理和渲染你的应用程序时可能会非常慢。在这篇文章中我想向你展示两种提高你的 Streamlit 应用程序速度的方法使用内置的Streamlit 缓存函数和使用内置的functools 缓存函数。这两种方法都基于缓存的概念即如果某件事之前已经被触发则输出会被保存以供以后重用。在进入结果之前我认为理解以下 3 个基本理论非常重要Streamlit、Streamlit 缓存和 functools 缓存在底层是如何工作的。PS所有图片均由我创作除非另有说明。Streamlit 每次都会重新执行一切。每次。每次。如介绍中所述Streamlit 使用简单但简单是有代价的。Streamlit 运行在一个独特的原则之上使其与其他许多网络框架区别开来每次用户与应用程序交互时整个脚本都会从头到尾重新执行。就是这一切。这种行为可能看起来很奇怪但它是 Streamlit 简单和强大的关键。开发者设计 Streamlit 每次都重新执行的原因之一是为了使其默认情况下“无状态”。由于脚本每次都是完全重新执行因此不需要显式地管理应用程序不同部分之间的状态。每次脚本运行都从一张白纸开始所有内容都是根据当前输入重新计算的。“无状态”很好但想象一下你有一个读取数据的函数。除非我们对此采取措施否则 Streamlit 将会每次都重新运行读取数据的函数。脚本重新执行是我们遇到速度性能问题的原因。然而有简单的方法可以解决这个问题。理解 Streamlit 缓存什么是 Streamlit 缓存Streamlit 的缓存机制允许你存储昂贵计算的结果以便在后续脚本执行中重用。有了缓存如果 Streamlit 知道一个函数或对象已经被调用过它将跳过执行并立即返回缓存的“结果”这可以显著加快你的应用速度。基本上你打破了 Streamlit 的无状态执行模型因为缓存允许应用的部分以有状态的方式运行这样结果可以在脚本重新运行之间持久化。Streamlit 提供了两个主要的缓存装饰器st.cache_data这个装饰器非常适合缓存与数据相关的操作例如加载数据集或查询数据库。这*“是所有返回数据的函数的默认命令 – 不论是 DataFrames、NumPy 数组、str、int、float 或其他可序列化类型。” (直接引用[3]中的内容)*st.cache_resource这个装饰器用于缓存资源例如机器学习模型其中资源需要初始化一次并多次重用。如何调用 Streamlit 缓存就像用**st.cache_data**装饰你的函数一样简单。st.cache_data()deffiltering_pandas(df:pd.DataFrame,dates_filterNone,device_filterNone,ROI_filterNone,market_filterNone)-pd.DataFrame:# filtering operations...returndf⚠️请注意⚠️在上面的函数中我们有 5 个输入。Streamlit 会将这 5 个输入的任何组合视为一个新的缓存对象。例如如果device_filterDesktop或device_filterMobileStreamlit 将缓存两个不同的 dataframe 输出。你可以想象这会在内存大小方面如何爆炸。设置约束以控制你的缓存内存使用这里是 Streamlit 推荐的两个约束条件ttl 存活时间。其想法是强制 streamlit 在一段时间内使用缓存。“如果时间到了你再次调用函数应用将丢弃任何旧的缓存值并重新运行该函数。”(直接引用[3]中的内容)*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(maxsize128)deffiltering_pandas(dates_filterNone,device_filterNone,ROI_filterNone,market_filterNone):# filtering operations...returndf⚠️小心⚠️你是否注意到functools.lru_cache装饰的filtering_pandas()函数没有df()dataframe 作为输入将此函数与st.cache_data()示例中使用的函数进行比较你就会看到区别。原因是由于可哈希对象。可哈希对象。如果你使用functools缓存你将遭受的痛苦。functools.lru_cache装饰器要求传递给缓存函数的所有参数都是可哈希的。一个 dataframe 是不可哈希的因为它是可以变的。这是在编写带有functools.lru_cache装饰器的函数时遇到的一个痛点。我将在稍后介绍我是如何处理这个问题的。基准测试设计练习在介绍了这两种缓存方法之后是时候比较两者的性能了。为此我创建了一个以下基准测试练习我创建了一系列合成 dataframe行数从 1,000 到 10,000,000。我创建了一个典型的 ETL 流程其中我们加载数据过滤它将其与另一个 dataframe 连接并根据一个段进行聚合。这些 ETL 函数已经用以下方式编写1Pandas 2Polars 3带有 Streamlit 缓存的缓存 pandas 函数 4带有 functools 缓存的缓存 pandas 和缓存 polars 函数。我将这些封装到一个 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_dfNone,dates_filterNone,device_filterNone,market_filterNone,ROI_filterNone,list_of_grp_by_fieldsNone,):dfread_and_combine_csv_files_pandas(folder_path)dffiltering_pandas(dfdf,dates_filterdates_filter,device_filterdevice_filter,market_filtermarket_filter,ROI_filterROI_filter)dfjoin_pandas(df,secondary_df)dfaggregating_pandas(dfdf,list_of_grp_by_fieldslist_of_grp_by_fields)returndfst.cache_data()defpandas_etl_streamlit_cached(folder_path,secondary_dfNone,dates_filterNone,device_filterNone,market_filterNone,ROI_filterNone,list_of_grp_by_fieldsNone,):dfread_and_combine_csv_files_pandas_cached(folder_path)dffiltering_pandas_cached(dfdf,dates_filterdates_filter,device_filterdevice_filter,market_filtermarket_filter,ROI_filterROI_filter)dfjoin_pandas_cached(df,secondary_df)dfaggregating_pandas_cached(dfdf,list_of_grp_by_fieldslist_of_grp_by_fields)returndf在查看过滤函数时这是我编写的代码构建基本的 pandas / python 过滤函数。因为我在为 Streamlit 编写函数所以我拥有所有那些可选参数这些参数将是用户输入的内容如果没有输入则默认为None。装饰一个调用非缓存函数的函数。如我所说你不需要在你的最终应用程序中这样做但我建议你以这种方式对缓存装饰器进行基准测试。deffiltering_pandas(df:pd.DataFrame,dates_filterNone,device_filterNone,ROI_filterNone,market_filterNone)-pd.DataFrame:ifdates_filter:# Ensure the filter dates are datetime objectsdf[Date]pd.to_datetime(df[Date])start_datepd.to_datetime(dates_filter[0])end_datepd.to_datetime(dates_filter[1])dfdf[(df[Date]start_date)amp;(df[Date]end_date)]ifdevice_filter:dfdf[df[Device].isin(device_filter)]ifmarket_filter:dfdf[df[Market].isin(market_filter)]ifROI_filter:dfdf[(df[ROI]ROI_filter[0])amp;(df[ROI]ROI_filter[1])]returndfst.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的函数的方式。如果你可以缓存一个不可变对象就去做。例如读取从 csv 文件中的数据集是不可变的。如果你的函数尝试对数据框进行某些操作你有两种选择选项 A 是对数据框进行哈希并将其作为输入参数传递。这可能会降低你的速度性能因为你在对数据框进行哈希但你可以在 Streamlit session_state 变量中保存这个哈希对象以供以后重用。选项 B 是简单地调用缓存函数你的数据是从那里读取的并运行整个 ETL。这不太符合编程标准但如果工作足够小我不认为这有什么问题。如果你的函数需要其他输入请将这些输入设置为不可变。这与选项 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_filterNone,device_filterNone,market_filterNone,ROI_filterNone,list_of_grp_by_fieldsNone):# 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.dfread_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_datepd.to_datetime(dates_filter[0])end_datepd.to_datetime(dates_filter[1])dfdf[(df[Date]start_date)amp;(df[Date]end_date)]ifdevice_filter:dfdf[df[Device].isin(device_filter)]ifmarket_filter:dfdf[df[Market].isin(market_filter)]ifROI_filter:dfdf[(df[ROI]ROI_filter[0])amp;(df[ROI]ROI_filter[1])]markets_pandas_dfpd.read_csv(synthetic_data/data_csv/dataset_markets/markets.csv)dfpd.merge(df,markets_pandas_df,onMarket,howinner)iflist_of_grp_by_fields:df(df.groupby(list(list_of_grp_by_fields)).agg({**{field:sumforfieldinsum_fields},**{field:meanforfieldinmean_fields}}))# Rename columns to clarify which operation was performeddf.columns[f{col}_{Sumifcolinsum_fieldselseAvg}forcolindf.columns]dfdf.reset_index()returndf# We create inmutable objects from lists by creating tuplesimmutable_device_filtertuple(device_filter)ifdevice_filterelseNoneimmutable_market_filtertuple(market_filter)ifmarket_filterelseNoneimmutable_ROI_filtertuple(ROI_filter)ifROI_filterelseNoneimmutable_list_of_grp_by_fieldstuple(list_of_grp_by_fields)iflist_of_grp_by_fieldselseNonepandas_functools_cached_dfpandas_functools_etl(folder_pathfolder_path,dates_filterdates_filter,device_filterimmutable_device_filter,market_filterimmutable_market_filter,ROI_filterimmutable_ROI_filter,list_of_grp_by_fieldsimmutable_list_of_grp_by_fields,)上述代码就是这样工作的尽管你可以通过注释来跟踪它读取数据函数被缓存。任何时候任何调用此读取方法的函数缓存都会启动并且不需要进行读取。上述示例遵循选项 B其中我没有对数据框进行哈希并添加为输入参数而是直接调用了读取方法。最后尽管函数的输入以及 pandas 使用如 _marketfilter等参数的方式使其看起来像列表但我们实际上传递的是一个包含列表的元组。这样我们就“欺骗”了缓存操作让它认为它是一个不可变对象。基准测试结果而现在就是真相大白的时候… 第一次运行顺便说一下绝对执行时间本身并不太重要取决于你的机器等但了解我们正在处理哪些基线是很好的。下面的图表显示了使用指定行数的 ETL 在每个选项下第一次运行所需的时间。在这里缓存还没有启动。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/aed7776bdfc40997aa9bf51efc12cae1.png第一次运行的执行速度。你可以看到对于较小的数据集polars 与 pandas 之间没有区别所有框架都少于 1 秒。但对于较大的数据集仅使用 polars 就能大幅提高效率见 10m其中 pandas 需要 8 秒而 polars 大约 1 秒。我们已知 polars 比 pandas 快但嘿总是好事。如果我们关注较大的数据集你可以观察到两种缓存方法都会给非缓存方法增加一点延迟。例如对于 10m 行数据集pandas 运行 ETL 需要 8 秒functools 需要 9.5 秒比 pandas 非缓存版本慢 16%streamlit 需要 10.5 秒_ 比 pandas 非缓存版本慢 24%_ 这是因为你的机器在第一次看到这些内容时需要一些时间来缓存它们。第二次运行对于第二次运行我们将关注更大的数据集。正如所见对于较小的数据集你可能甚至不需要担心所有这些缓存操作。当我第二次重新运行整个操作时在速度性能方面结果绝对令人难以置信。让我们分别比较 pandas 和 polars 的速度提升因为我们感兴趣的是基于缓存的性能提升而不是 polars 与 pandas 的比较。Pandashttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c423b5f450c33f099596e5ea96493cfa.pngpandas ETL 的速度提升Polarshttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/74a0fcdc52f394be604cff5a2845fe5d.pngpolars 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-06https://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-06https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9b27117c56aad89c9f16f8c14578915a.png执行速度的视觉展示摘要在这篇文章中我们讨论了 Streamlit、Streamlit 缓存和 functools 缓存在底层是如何工作的理论。此外我还提供了一些关于如何编写 Streamlit 缓存和 functools 缓存的示例。我们最终进行了一次基准测试并看到了与不缓存相比缓存是多么地快。特别是对于 functools。TLDR 版本如下Streamlit 缓存编写起来更容易但如果您需要巨大的改进收益或者您正在使用 polars那么 functools 缓存应该是您的首选选项。PSStreamlit 缓存与 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